[英]Automatically add type signatures to top-level functions
我很懶,寫了一個Haskell模塊(使用優秀的EclipseFP IDE)而沒有給我的頂級函數提供類型簽名。
EclipseFP使用HLint自動標記每個違規函數,我可以通過4次鼠標點擊來修復每個函數。 有效,但乏味。
是否有一個實用程序將掃描.hs文件,並發出一個修改版本,為每個頂級函數添加類型簽名?
例:
./addTypeSignatures Foo.hs
會讀取文件Foo.hs
:
foo x = foo + a
並發出
foo :: Num a => a -> a
foo x = x + 1
如果工具自動編輯Foo.hs
並保存備份Foo.bak.hs
則獎勵積分
emacs的haskell-mode具有插入函數類型簽名的快捷方式:Cu,Cc,Ct。 它不是自動的,您必須為每個功能執行此操作。 但是如果你只有一個模塊,可能需要幾分鍾的時間來完成它。
以下是上述腳本的變體,根據ehird的評論使用“:browse”而不是“:type”。
此解決方案的一個主要問題是“:browse”顯示完全限定的類型名稱,而“:type”使用導入的(縮寫的)類型名稱。 這樣,如果您的模塊使用非限定導入類型(常見情況),則此腳本的輸出將無法編譯。
這種短缺是可以解決的(使用一些進口解析),但這個兔子洞正在變得越來越深。
#!/usr/bin/env perl
use warnings;
use strict;
sub trim {
my $string = shift;
$string =~ s/^\s+|\s+$//g;
return $string;
}
my $sig=0;
my $file;
my %funcs_seen = ();
my %keywords = ();
for my $kw qw(type newtype data class) { $keywords{$kw} = 1;}
foreach $file (@ARGV)
{
if ($file =~ /\.lhs$/)
{
print STDERR "$file: .lhs is not supported. Skipping.\n";
next;
}
if ($file !~ /\.hs$/)
{
print STDERR "$file is not a .hs file. Skipping.\n";
next;
}
my $module = $file;
$module =~ s/\.hs$//;
my $browseInfo = `echo :browse | ghci $file`;
if ($browseInfo =~ /Failed, modules loaded:/)
{
print STDERR "$browseInfo\n";
print STDERR "$file is not valid Haskell source file. Skipping.\n";
next;
}
my @browseLines = split("\n", $browseInfo);
my $browseLine;
my $func = undef;
my %dict = ();
for $browseLine (@browseLines) {
chomp $browseLine;
if ($browseLine =~ /::/) {
my ($data, $type) = split ("::", $browseLine);
$func = trim($data);
$dict{$func} = $type;
print STDERR "$func :: $type\n";
} elsif ($func && $browseLine =~ /^ /) { # indent on continutation
$dict{$func} .= " " . trim($browseLine);
print STDERR "$func ... $browseLine\n";
} else {
$func = undef;
}
}
my $backup = "$file.bak";
my $new = "$module.New.hs";
-e $backup and die "Backup $backup file exists. Refusing to overwrite. Quitting";
open OLD, $file;
open NEW, ">$new";
print STDERR "Functions in $file:\n";
my $block_comment = 0;
while (<OLD>)
{
my $original_line = $_;
my $line = $_;
my $skip = 0;
$line =~ s/--.*//;
if ($line =~ /{-/) { $block_comment = 1;} # start block comment
$line =~ s/{-.*//;
if ($block_comment and $line =~ /-}/) { $block_comment=0; $skip=1} # end block comment
if ($line =~ /^ *$/) { $skip=1; } # comment/blank
if ($block_comment) { $skip = 1};
if (!$skip)
{
if (/^(('|\w)+)( +(('|\w)+))* *=/ )
{
my $object = $1;
if ((! $keywords{$object}) and !($funcs_seen{$object}))
{
$funcs_seen{$object} = 1;
print STDERR "$object\n";
my $type = $dict{$1};
unless ($sig)
{
if ($type) {
print NEW "$1 :: $type\n";
print STDERR "$1 :: $type\n";
} else {
print STDERR "no type for $1\n";
}
}
}
}
$sig = /^(('|\w)+) *::/;
}
print NEW $original_line;
}
close OLD;
close NEW;
my $ghciPostTest = `echo 1 | ghci $new`;
if ($ghciPostTest !~ /Ok, modules loaded: /)
{
print $ghciPostTest;
print STDERR "$new is not valid Haskell source file. Will not replace original (but you might find it useful)";
next;
} else {
rename ($file, $backup) or die "Could not make backup of $file -> $backup";
rename ($new, $file) or die "Could not make new file $new";
}
}
對於Atom編輯器,可以使用包haskell-ghc-mod自動為每個函數插入類型簽名,該包提供:
'ctrl-alt-T': 'haskell-ghc-mod:insert-type'
這是基於解析GHC -Wmissing-signatures
警告的另一個hacky嘗試,因此腳本不必解析Haskell。 它將警告轉換為sed腳本,該腳本執行插入並將其結果打印到stdout,或者如果給出-i
則將文件修改為原位。
需要下面配置的Stack項目,但您可以更改buildCmd
。
使用GHC 8.2.2和8.4.3嘗試使用的幾個文件,但是@misterbee的第一個答案中的警告同樣適用:)此外,如果它們產生不同格式的警告,它顯然會破壞舊的或更新的GHC(但是對我來說,更復雜的工具似乎也一直在打破,所以...)。
#!/bin/zsh
set -eu
setopt rematchpcre
help="Usage: ${0:t} [-d] [-i | -ii] HASKELL_FILE
Options:
-d Debug
-i Edit target file inplace instead of printing to stdout
(Warning: Trying to emulate this option by piping from
and to the same file probably won't work!)
-ii Like -i, but no backup
"
### CONFIG ###
buildCmd() {
touch $inputFile
stack build --force-dirty --ghc-options='-fno-diagnostics-show-caret -Wmissing-signatures'
}
# First group must be the filename, second group the line number
warningRegexL1='^(.*):([0-9]+):[0-9]+(-[0-9]+)?:.*-Wmissing-signatures'
# First group must be the possible same-line type signature (can be empty)
warningRegexL2='Top-level binding with no type signature:\s*(.*)'
# Assumption: The message is terminated by a blank line or an unindented line
messageEndRegex='^(\S|\s*$)'
### END OF CONFIG ###
zparseopts -D -E d=debug i+=inplace ii=inplaceNoBackup h=helpFlag
[[ -z $helpFlag ]] || { printf '%s' $help; exit 0 }
# Make -ii equivalent to -i -i
[[ -z $inplaceNoBackup ]] || inplace=(-i -i)
inputFile=${1:P} # :P takes the realpath
[[ -e $inputFile ]] || { echo "Input file does not exist: $inputFile" >&2; exit 2 }
topStderr=${${:-/dev/stderr}:P}
debugMessage()
{
[[ -z $debug ]] || printf '[DBG] %s\n' "$*" > $topStderr
}
debugMessage "inputFile = $inputFile"
makeSedScript()
{
local line
readline() {
IFS= read -r line || return 1
printf '[build] %s\n' $line >&2
}
while readline; do
[[ $line =~ $warningRegexL1 ]] || { debugMessage "^ Line doesn't match warningRegexL1"; continue }
file=${match[1]}
lineNumber=${match[2]}
[[ ${file:P} = $inputFile ]] || { debugMessage "^ Not our file: $file"; continue }
# Begin sed insert command
printf '%d i ' $lineNumber
readline
[[ $line =~ $warningRegexL2 ]] ||\
{ printf 'WARNING: Line after line matching warningRegexL1 did not match warningRegexL2:\n %s\n' $line >&2
continue }
inlineSig=${match[1]}
debugMessage "^ OK, inlineSig = $inlineSig"
printf '%s' $inlineSig
readline
if [[ ! ($line =~ $messageEndRegex) ]]; then
[[ $line =~ '^(\s*)(.*)$' ]]
indentation=${match[1]}
[[ -z $inlineSig ]] || printf '\\n'
printf ${match[2]}
while readline && [[ ! ($line =~ $messageEndRegex) ]]; do
printf '\\n%s' ${line#$indentation}
done
fi
debugMessage "^ OK, Type signature ended above this line"
# End sed insert command
printf '\n'
done
}
prepend() {
while IFS= read -r line; do printf '%s%s\n' $1 $line; done
}
sedScript="$(buildCmd |& makeSedScript)"
if [[ -z $sedScript ]]; then
echo "No type-signature warnings for the given input file were detected (try -d option to debug)" >&2
exit 1
fi
printf "\nWill apply the following sed script:\n" >&2
printf '%s\n' $sedScript | prepend "[sed] " >&2
sedOptions=()
if [[ $#inplace -ge 1 ]]; then
sedOptions+=(--in-place)
[[ $#inplace -ge 2 ]] || cp -p --backup=numbered $inputFile ${inputFile}.bak
fi
sed $sedOptions -f <(printf '%s\n' $sedScript) $inputFile
這個perl腳本在它上面做了一個黑客工作,對源文件結構做了一些假設。 (例如: .hs
文件(不是.lhs
),簽名緊接在定義之前的行,定義在左邊距處是齊平的,等等)
它試圖處理(跳過)注釋,方程式定義(帶有重復的左側),以及在ghci
中生成多行輸出的類型。
毫無疑問,許多有趣的有效案件處理不當。 該腳本並不接近尊重Haskell的實際語法。
它非常慢,因為它為每個需要簽名的函數啟動了一個ghci
會話。 它創建一個備份文件File.hs.bak
,將它找到的函數打印到stderr,以及缺少簽名的函數的簽名,並將升級后的源代碼寫入File.hs
它使用中間文件File.hs.new
,並進行一些安全檢查,以避免使用垃圾覆蓋您的內容。
自行承擔使用風險。
此腳本可能會格式化您的硬盤驅動器,燒毀您的房子,unsafePerformIO,並有其他不純的副作用。 事實上,它可能會。
我覺得很臟
在Mac OS X 10.6 Snow Leopard上測試了幾個我自己的.hs
源文件。
#!/usr/bin/env perl
use warnings;
use strict;
my $sig=0;
my $file;
my %funcs_seen = ();
my %keywords = ();
for my $kw qw(type newtype data class) { $keywords{$kw} = 1;}
foreach $file (@ARGV)
{
if ($file =~ /\.lhs$/)
{
print STDERR "$file: .lhs is not supported. Skipping.";
next;
}
if ($file !~ /\.hs$/)
{
print STDERR "$file is not a .hs file. Skipping.";
next;
}
my $ghciPreTest = `echo 1 | ghci $file`;
if ($ghciPreTest !~ /Ok, modules loaded: /)
{
print STDERR $ghciPreTest;
print STDERR "$file is not valid Haskell source file. Skipping.";
next;
}
my $module = $file;
$module =~ s/\.hs$//;
my $backup = "$file.bak";
my $new = "$module.New.hs";
-e $backup and die "Backup $backup file exists. Refusing to overwrite. Quitting";
open OLD, $file;
open NEW, ">$new";
print STDERR "Functions in $file:\n";
my $block_comment = 0;
while (<OLD>)
{
my $original_line = $_;
my $line = $_;
my $skip = 0;
$line =~ s/--.*//;
if ($line =~ /{-/) { $block_comment = 1;} # start block comment
$line =~ s/{-.*//;
if ($block_comment and $line =~ /-}/) { $block_comment=0; $skip=1} # end block comment
if ($line =~ /^ *$/) { $skip=1; } # comment/blank
if ($block_comment) { $skip = 1};
if (!$skip)
{
if (/^(('|\w)+)( +(('|\w)+))* *=/ )
{
my $object = $1;
if ((! $keywords{$object}) and !($funcs_seen{$object}))
{
$funcs_seen{$object} = 1;
print STDERR "$object\n";
my $dec=`echo ":t $1" | ghci $file | grep -A100 "^[^>]*$module>" | grep -v "Leaving GHCi\." | sed -e "s/^[^>]*$module> //"`;
unless ($sig)
{
print NEW $dec;
print STDERR $dec;
}
}
}
$sig = /^(('|\w)+) *::/;
}
print NEW $original_line;
}
close OLD;
close NEW;
my $ghciPostTest = `echo 1 | ghci $new`;
if ($ghciPostTest !~ /Ok, modules loaded: /)
{
print $ghciPostTest;
print STDERR "$new is not valid Haskell source file. Will not replace original (but you might find it useful)";
next;
} else {
rename ($file, $backup) or die "Could not make backup of $file -> $backup";
rename ($new, $file) or die "Could not make new file $new";
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.