Edited at

haskellでshっぽい言語を作成した

ちびちび作っていたhaskell製のshellスクリプトがまともに動くようになった。

紆余曲折あり、全くposixのshとは別の言語になってしまった。


コード例


fizzbazz

seq 1 100 | loop {

read || break $ ^{
($1%15=0) &&& echo FizzBuzz
($1%3=0) &&& echo Fizz
($1%5=0) &&& echo Buzz
echo $1
}
}

または

seq 1 100 | loop {

read || break $ ^(
($1%15=0 ?) FizzBuzz
($1%3=0 ?) Fizz
($1%5=0 ?) Buzz $1
) $ echo
}


階乗

def fact ^(($1=1 ?) 1 ($1 * fact ($1-1)))


改善点


速度

フィボナッチ数列の計算だけならdashの3倍程度の速度で動作する。

$ cat test/fib

def fib ^(($1<3 ?) 1 (fib ($1-2) + fib ($1-1)))
fib $1 $ echo
$
$ time stack exec snail test/fib 37
24157817

real 0m35.557s
user 0m43.174s
sys 0m3.160s

$ cat test/fib.dash

set -f

fib() {
local n=$1
[ $n -lt 3 ] && {
RET=1
return
}
fib $((n-1))
local k=$RET
fib $((n-2))
RET=$((k+RET))
}

fib $1
echo $RET
$
$
$ time dash test/fib.dash 37
24157817

real 1m44.144s
user 1m44.140s
sys 0m0.004s

コードが全く違うので不公平な気がする。

抽象構文木をそのまま実行する方法の場合、大体shellと同じ計算速度になるが、抽象構文木を作らずにラムダ式に変換すると何倍か早くなる。(遅延評価のままだとメモリを食いすぎてか長いコードでは死んでしまう模様)

なお、dashと比較しているので文章上速い感じになっているがrubyだと数秒で終わる。


スコープ

レキシカルスコープだが、変数がイミュータブルなので、クロージャでカウンタを作るような真似はできない。

関数・変数の参照には検索木であるhashmapを使用している。

性能上ボトルネックとなるが、データ構造がイミュータブルなので特別な仕組み無しでレキシカルスコープを実現できる利点がある。


変数展開

この言語には単語分割もグロブ展開もないので、変数にダブルクォートなどつけなくても空白文字を扱える。

※"@"はプロンプト

@ let a "' '"

@ ^{echo $1} $a
' '

コマンドオプション等を変数に入れた場合など、存在しない、または複数の値を持つものとして変数を展開したい場合がある。

この言語はコレクション型を多値として扱うための文法を持っており、以下のように使用する。($@との類似性から@をつける)

@ let none []

@ ls $none
ls: '' にアクセスできません: そのようなファイルやディレクトリはありません
@ ls $@none
ChangeLog.md LICENSE README.md app package.yaml snail.cabal src stack.yaml test

@ let a [1 2 3]

@ ls $a
ls: '1 2 3' にアクセスできません: そのようなファイルやディレクトリはありません
@ ls $@a
ls: '1' にアクセスできません: そのようなファイルやディレクトリはありません
ls: '2' にアクセスできません: そのようなファイルやディレクトリはありません
ls: '3' にアクセスできません: そのようなファイルやディレクトリはありません


戻り値

諸々の事情により、shの貧弱な戻り値を拡張した。この言語では普通の言語のように扱えるすべてのデータ構造を戻り値に与えることができる。

read関数は標準出力から一行受け取ってそれを戻り値にする。

@ echo a | read

@ echo $?
a
@ echo a | read $ printf '%s is got\n'
a is got

"$"はelixirのパイプ演算子(|>)のようなものだ。


データ構造

リストと連想配列が使える。

@ echo ([a b c] 2)

b
@ echo (#[a 1 b 2 c 3] b)
2


算術式

色々と算術式に詰め込んでいる。

文字列のマッチングもできる。

@ ^{ ($1~'[a-z]+') &&& echo small

($1~'[A-Z]+') &&& echo large
echo otherwise
} ABC
large

算術式で定義した関数は算術式内でも呼び出せる。

@ def fib ^(($1<3 ?) 1 (fib ($1-2) + fib ($1-1)))

@ echo (fib 10)
55

上記のように?演算子を使用して、3項演算子のようなこともできる。


なんとも言えない点


状態と戻り値

戻り値を拡張した結果として状態を戻り値で表すことができなくなった。

そのため、戻り値とは別に状態をセットしてやる必要がある。

@ {true 1} && echo $?

1
@ {false 0} || echo $?
0


グロビング

ブロブ展開は一致する文字列がない場合の処理と、変数展開時やコマンド置換時など不意に動作するなど仕様に不満があり、削除した。glob関数で明示する必要がある。

@ glob * $$ echo

app package.yaml README.md snail.cabal LICENSE test ChangeLog.md src stack.yaml
@ glob aaa $$ echo
@


多値

shell系言語との取り合わせが良いように感じ、なんとなく導入した。


  • メリット


    • ラッパー系の関数(timeoutコマンドみたいなやつ)など、複数の関数の戻り値を扱いたいケースでコードが自然になる。

    • 戻り値がないことが自然な関数に無理に戻り値を与えてやる必要がない。



  • デメリット


    • 戻り値が0個のケースが出てくるため、算術式との取り合わせが悪い。

    • 戻り値を取ろうとするたびに身構える必要がある。



戻り値0個は言語仕様への影響が大きすぎる気がする。


haskellに関する所感


ステップ数

haskell初心者が2Kで実装できた。


シグナルハンドラとプロセスフォーク

フォークするとたまに(多分ガーベッジコレクションの具合で)内部エラーが出て子プロセスが死亡する。対話モードのときしか起きないのでhaskelineが行っているシグナルハンドリングの問題のような気がしている。

@ fork {sleep 1; echo}

@ snail: internal error: evacuate: strange closure type 5274697
(GHC version 8.4.4 for x86_64_unknown_linux)
Please report this as a GHC bug: http://www.haskell.org/ghc/reportabug