Автоматически добавлять сигнатуры типов к функциям верхнего уровня

Я был ленив и написал модуль Haskell (используя отличную среду EclipseFP) без предоставления подписей типа к моим функциям верхнего уровня.

EclipseFP использует HLint для автоматической маркировки каждой оскорбительной функции, и я могу исправить каждый из них с помощью четырех щелчков мыши. Эффективный, но утомительный.

Есть ли утилита, которая сканирует файл .hs и испускает измененную версию, которая добавляет сигнатуры типов к каждой функции верхнего уровня?

Пример:

./addTypeSignatures Foo.hs 

будет читать файл Foo.hs:

foo x = foo + a

и испускать

foo :: Num a => a -> a
foo x = x + 1

Бонусные очки, если инструмент автоматически редактирует Foo.hs на месте и сохраняет резервную копию Foo.bak.hs

Ответ 1

Существует haskell-режим для emacs, который имеет ярлык для вставки типа сигнатуры функции: C-u, C-c, C-t. Это не автоматическое, вы должны сделать это для каждой функции. Но если у вас есть только один модуль, вам, вероятно, потребуется несколько минут, чтобы пройти через него.

Ответ 2

Здесь приведен вариант выше script, который использует ": browse" вместо ": type", за один комментарий.

Одна из основных проблем с этим решением заключается в том, что ": browse" отображает полностью квалифицированные имена типов, тогда как ": type" использует импортированные (сокращенные) имена типов. Это, если в вашем модуле используются неквалифицированные импортированные типы (общий случай), вывод этого script не будет компилироваться.

Этот недостаток является фиксируемым (с использованием некоторого анализа импорта), но это отверстие кролика становится глубоким.

#!/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";
  }
}

Ответ 3

Этот perl script выполняет хакерскую работу, делая некоторые предположения о структуре исходного файла. (Например: .hs file (not .lhs), подписи находятся в строке, непосредственно предшествующей определениям, определения - на краю левого поля и т.д.)

Он пытается обрабатывать (пропускать) комментарии, определения стиля уравнения (с повторяющимися левыми сторонами) и типы, которые генерируют многострочный вывод в ghci.

Несомненно, многие интересные действительные случаи не обрабатываются должным образом. script не близок к соблюдению фактического синтаксиса Haskell.

Это невероятно медленно, поскольку он запускает сеанс ghci для каждой функции, для которой требуется подпись. Он создает резервный файл File.hs.bak, печатает функции, которые он находит, для stderr, а также подписи для функций, отсутствующих подписи, и записывает обновленный исходный код на File.hs. Он использует промежуточный файл File.hs.new и имеет несколько проверок безопасности, чтобы избежать перезаписи вашего содержимого с помощью мусора.

ИСПОЛЬЗУЙТЕ СВОЙ СОБСТВЕННЫЙ РИСК.

Этот script может отформатировать ваш жесткий диск, записать ваш дом вниз, небезопасныйPerformIO и иметь другие нечистые побочные эффекты. На самом деле это, вероятно, будет.

Я чувствую себя настолько грязным.

Протестировано на 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";
  }
}

Ответ 4

Для редактора Atom можно автоматически вставить подпись типа для каждой функции с пакетом haskell-ghc-mod, который обеспечивает:

 'ctrl-alt-T': 'haskell-ghc-mod:insert-type'

https://atom.io/packages/haskell-ghc-mod#keybindings