I was lazy and wrote a Haskell module (using the excellent EclipseFP IDE) without giving type signatures to my top-level functions.
EclipseFP uses HLint to automatically label every offending function, and I can fix each one with 4 mouse clicks. Effective, but tedious.
Is there a utility program that will scan a .hs file, and emit a modified version that adds type signatures to each top-level function?
Example:
./addTypeSignatures Foo.hs
would read a file Foo.hs
:
foo x = foo + a
and emit
foo :: Num a => a -> a
foo x = x + 1
Bonus points if the tool automatically edits Foo.hs
in place and saves a backup Foo.bak.hs
A function signature (or type signature, or method signature) defines input and output of functions or methods. A signature can include: parameters and their types. a return value and type. exceptions that might be thrown or passed back.
Function signatures provide a much more powerful and specific way to think about function usage and types. We can write signatures for functions that take a fixed number of arguments: circle : number, string, string -> image.
A function signature includes the function name, its arguments, and in some languages, the return type.
In C and C++, the type signature is declared by what is commonly known as a function prototype. In C/C++, a function declaration reflects its use; for example, a function pointer with the signature (int)(char, double) would be called as: char c; double d; int retVal = (*fPtr)(c, d);
There's haskell-mode for emacs that has a shortcut to insert type signature of a function: C-u, C-c, C-t. It is not automatic, you have to do it for each function. But if you have only one module, it will probably take you a few minutes to go through it.
Here's a variation of the above script, that uses ":browse" instead of ":type", per ehird's comment.
One major problem with this solution is that ":browse" displays fully qualified type names, whereas ":type" uses the imported (abbreviated) type names. This, if your module uses unqualified imported types (a common case), the output of this script will fail compilation.
That shortoming is fixable (using some parsing of imports), but this rabbit hole is getting deep.
#!/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";
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With