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

Я был ленив и написал модуль на 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


person misterbee    schedule 22.01.2012    source источник
comment
Команда hs-lint в Emacs будет автоматически применять предложения, если hs-lint-replace-without-ask установлено в t. Я не уверен, как ограничить его только подписями типов, но, безусловно, должен быть способ. И я публикую это только как комментарий, потому что это не решение EclipseFP.   -  person Jon Purdy    schedule 22.01.2012


Ответы (5)


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

person Vagif Verdi    schedule 22.01.2012
comment
Это правда, но это лишь немногим лучше, чем решение EclipseFP+hlint. - person Dan Burton; 23.01.2012

Вот вариант приведенного выше скрипта, который использует «: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";
  }
}
person misterbee    schedule 28.01.2012

Вот еще одна попытка взлома, основанная на анализе предупреждений GHC -Wmissing-signatures, поэтому скрипту не нужно анализировать Haskell. Он преобразует предупреждения в сценарий sed, который выполняет вставки и выводит результат на стандартный вывод или изменяет файл на месте, если указано -i.

Требуется проект стека, настроенный ниже, но вы можете изменить файл buildCmd.

Работает с несколькими файлами, которые я пробовал с GHC 8.2.2 и 8.4.3, но применяются те же предупреждения, что и в первом ответе @misterbee :) для меня более сложные инструменты, похоже, тоже все время ломаются, так что...).

#!/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
person FunctorSalad    schedule 20.09.2018

Этот 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";
  }
}
person misterbee    schedule 23.01.2012
comment
Вы сможете сделать это намного быстрее, загрузив файл в GHCi только один раз и используя :browse. - person ehird; 23.01.2012
comment
@ehird: Спасибо за указатель. Я опубликовал еще один ответ, в котором используется :browse, но есть другие проблемы :-( - person misterbee; 29.01.2012

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

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

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

person madnight    schedule 24.09.2016