自动为顶层函数添加类型签名

14

我很懒,使用优秀的EclipseFP IDE编写了一个Haskell模块,但没有为我的顶层函数提供类型签名。

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,那就更好了。


5
如果将hs-lint-replace-without-ask设置为t,Emacs中的hs-lint命令将自动应用建议。我不确定如何将其限制为仅适用于类型签名,但肯定有一种方法。我之所以只是发布评论,是因为这不是EclipseFP解决方案。 - Jon Purdy
5个回答

6

对于emacs,有一个名为haskell-mode的工具,可以通过以下快捷键插入函数类型签名:C-u, C-c, C-t。它不是自动的,您需要为每个函数执行此操作。但如果您只有一个模块,可能只需要几分钟即可完成。


这是事实,但它只比EclipseFP + hlint的解决方案好了一点点。 - Dan Burton

1
以下是以上脚本的变体,它使用":browse"而不是":type",根据ehird的评论。
这个解决方案的一个主要问题是":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";
  }
}

1
这是另一种基于解析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

0

0

这个 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";
  }
}

你应该只需将文件加载到 GHCi 一次并使用 :browse,就能使它运行得更快。 - ehird
@ehird:谢谢指引。我发了另一个使用“:browse”的答案,但还有其他问题 :-( - misterbee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接