開発日記などが長くなってきたので、古いものを置いておく場所。
昔のもの。
まだ作るとは決めてないのだけれど、なんとなく簡単なASTからmainを出力くらいしてみた。
現状は概念的には以下みたいなASTをコンパイルしている。
import "fmt"
let main () =
GoEval "fmt.Println(\"Hello World\")"
GoEvalは今はunit型としているが、any型にしてキャストを実装すれば割となんでも出来そうな気がする。 ちなみに以下みたいな関数もコンパイル出来た。
let hello (msg:string) =
GoEval "fmt.Printf(\"Hello %s\n\", msg)"
GoEvalの中で引数のmsgを(気を付けて)使う事が出来る。 ただ現状はこのhelloをmainから呼ぶ事が出来ない。
ここから先は型システムと呼び出し周りの処理を作る必要があって、そこがちょっとかったるい。 そうしないと関数の呼び出しが部分適用か判断出来ず、そこがfolangの根幹なのでとりあえずの妥協がしづらい。
ただそこさえ乗り切ってしまえば、Evalでラップしてかなりいろいろ書ける気がする。
型アノテーション、レコード型、パターンマッチ、パーサー、型推論など、いろいろと必要なものはあるけれど、 関数呼び出しと型アノテーションの周りだけ作ってしまえば、 あとはちょっとずつ進められそうな手応えを感じている。
ようするに型システムの所だよなぁ、かったるいのは。
どっかでちょっと1週末くらい頑張れば、やっていける気もするが。
理想的にはmini-folangをgoで作って、それでfolangのコンパイラを書いていく感じにしたいよな。 mini-folangに何が必要なのかは良く分からないので、まず最低限のmini-folangを作ってみて、 それをfolangで実装するのに必要な機能から足していく感じにしたい。
最低限というと、関数呼び出し周辺とパーサーかな。 あれ?もう関数呼び出し作ったら次パーサーか。もっとプリミティブとか作っていく気だったが、 確かになんのプリミティブが必要かって最初の段階だと良く分からないもんな。 先にパーサーを作って必要なデータ型を考える方がいいか。
パーサーをfolangで再実装する所まで行ったら、本格的に作っていける気はするな。
関数呼び出しとキャストの実装が出来たらGitHubにレポジトリ作るか。
リストとかはいらないかなぁ、という気はしているんだよな。スライスとタプルでやっていきたい。 タプルどうするんだ問題はまだ未解決なんだが。
forは出来たらそのまま使って、それをいい感じにラップしてその上ではfolangっぽく書けるような感じにしたいんだよな。 ML系シンタックスとfor文は相性悪そうだが。 再帰で頑張るならリストは便利という話はあるんだが、 基本的にfolangが欲しいのはパイプ演算子でSliceなどを処理していきたいからであって、 ループ回さざるをえないような処理はgolangで不満は無いんだよな。 goでさくっとfor文のコードを書いて、それをfolangから呼ぶようにしたい気がする。
そもそもに本当にF# のような関数呼び出し周りのgolangのような言語があったら便利なのかは、使ってみないと良く分からない。 使ってみないと必要か分からないのでなかなか作ろうという気が湧かない、 というブートストラップ問題があるんだよなぁ。 それがこのエントリのタイトルに「(?)」がついてしまう理由でもあって。
でもパーサーくらい書けば判断は出来ると思うので、そこまではやってみたい気がする。
型を解決するのが面倒と思っていたが、解決済みの型を渡す事で呼び出すコードは出来る、 という事に気づき、やってみる。30分くらい掛かったが無事動いた。
mainの中のhello変数の所には型も手で渡しているが、以下に相当するものが動いた。
package main
import "fmt"
let hello (msg:string) =
GoEval "fmt.Printf(\"Hello %s\\n\", msg)"
let main () =
hello "World"
まだ部分適用は実装してないので引数が足りないとpanicで落ちるが。
意外とこの切り口は悪くない気がしていて、「結構な面倒」を「まぁまぁの面倒3つ」くらいにうまく分割出来て、そのうちの一つを片付けられた気がする。 残りも一つ一つはやってもいいかな、というくらいの小ささに出来た。
ちゃんと作るのは面倒そうなので、どんな手応えかを調べるためのハリボテとして手抜きで作っているつもりだったのだが、 結果としては意外とちゃんとした実装になっているんだよな。 あとはコンパイル時の変数の辞書を作れば、割とこのくらいのスクリプトは動く実装になる。
型の辞書を実装したら、パーサーを書き始めてもいいかもしれない。
letはexpressionにせずにstmtにしてしまっていいかな、と思っているがどうだろう?結局goの変数宣言になるので、 生成結果とあんまり違うツリーとしておくのは良くないかな、と。 ReasonMLとかこの辺はどうなっているのかなぁ。
オフサイドルールは最初から実装しておきたいな。どうせそれしか使わないので。
golang慣れてないので、型に相当するもののinterfaceに対応するメソッドを、ポインタで実装すべきか実体で実装すべきかいまいち分かってない。 とりあえず全部ポインタに統一しているが、C++ならこのくらいのstructは実体で持って回る方が普通なんだよなぁ。 ただC++はvirtualは全部ポインタ越しになるんだよなぁ。
関数定義で型をmapに登録して関数呼び出しでこのmapをlookupするようにする。 これでASTより先は一番原始的なものは完成だ。
次はパーサーだな。 パーサーまで作れば、最初の目標とすべきターゲットが出来た事になる。なんか意外とすぐだったな。
現状は以下くらい。
% wc ftype.go main.go
84 191 1856 ftype.go
286 678 5296 main.go
370 869 7152 total
パーサーがどのくらいかはわからんが、このくらいをトランスパイルで作るくらいならなんか出来そうな気もしてくるな。 思ったよりも行けそうかも?
トークナイザを書く。オフサイドルールとかを入れようとしたら意外と良く分からない感じだったので、 まずはそういう事を考えずにトークナイズだけ行う。
まずは一番カンタンなhello world的な以下に必要な事だけやろうとしているが、
package main
import "fmt"
let main () =
GoEval "fmt.Println(\"Hello World\")"
意外と必要なものが多く、思ったよりも時間が掛かった。文字列リテラルのエスケープが開幕に必要になってしまうのだよなぁ。 ただなんとか一通り終わった。
次はパーサーを書くのだが、ちょっと燃え尽きたので休憩。ここまでやった程度のASTを作るパーサーまではあと一歩って所かな。
そのあと休み休み進めて、上記のコードがパース出来る所までは出来た。parseExprはかなり手抜きだが。
あとは関数定義の引数のパースと関数呼び出しのパースを追加すれば、ここまで作ったastを一通り生成出来るようになるな。
夜に気が向いたので続きを実装し、関数定義の引数のパースと関数呼び出しのパースを実装。 これで以下がトランスパイル出来るようになった。
package main
import "fmt"
let hello (msg:string) =
GoEval "fmt.Printf(\"Hello %s\\n\", msg)"
let main () =
hello "World"
おもちゃではあるが、関数定義と関数呼び出しが出来るようになったので、最低限の処理系とは言えるんじゃないか。
ここまでのトランスパイラと同じものをfolangで再実装して、それを動かすのに必要な機能を追加していく、というのを次のフェーズとしたい。 ここまでのトランスパイラのコード行数は以下。
% wc ast.go ftype.go main.go parser.go transpiler.go
193 466 3645 ast.go
84 191 1856 ftype.go
17 26 242 main.go
454 1064 7367 parser.go
94 218 1636 transpiler.go
842 1965 14746 total
840行くらい。 それなりにいろいろな機能を使っているので楽では無いが、そうはいっても所詮840行なのでこのくらいなら作れるんじゃないか?という気もしてくるな。 3000行くらいで行けるんじゃないか?甘いか?
なんかfolangでfolangのコンパイラを書く所までは行けそうな気がしてきたな。 セルフトランスパイルまで行けばちょっとしたものじゃないか?
ここをスタート地点としよう、という事で公開する。 karino2/folang: Funcitonal language transpiler to golang.
まだ置いただけだが。
次はレコードとdiscriminated unionとパターンマッチの3つをやらないといけなくて、3つ同時にやらないといけないとやる気が出ない。 1つだけやればいいんだろうが、1つだと使い道が無いからなぁ。
レコード型だけやればいいんだが、これだけでやりたい事が無いのでやる気が出ないんだよなぁ。
F#のリファレンスのレコード型のところの例をとりあえず動かすか?
以下のコードが
type Point = { X: int; Y: int }
let mypoint = {X = 1; Y = 2 }
以下になればいいかな。
type Point struct {
X int
Y int
}
mypoint := &Point{ X: 1, Y: 2 }
でもmypointの使い道が無いんだよな。 必要な機能は大量にあるが最低限これだけ実装すればこれが出来る、というのが見えてないのでやる気が出ない。
とりあえずこれくらいとフィールドアクセスだけ実装してunionの実装を進めるのがいいのかなぁ。 なんか最初のターゲット(トランスパイラ)が、最初のターゲットにしては難しすぎるような気がしてきた。うーん。
とりあえず fun and profitの方のレコード型のページのリンクも貼っておこう。 Records - F# for fun and profit
やることをリストアップしておく。
Recordの実装は以下。
よし、なんかやる事が細分化出来たらやる気が出てきた。ぼちぼちやっていこう
Recordのフィールドアクセス以外をなんとなく実装。 以下がトランスパイル出来るように。
type hoge = {X: string; Y: string}
let ika () =
{X="abc"; Y="def"}
トランスパイルして以下。
type hoge struct {
X string
Y string
}
func ika() *hoge{
return &hoge{X: "abc", Y: "def"}
}
レコード型はポインタ型として扱う事に。というかプリミティブとスライスやマップ以外は全部ポインタにする。 ポインタと実体を扱うのは無理という事で。 ポインタにするとequalityをどうするか問題はあるが、レコードのequalityは結局はカスタムに実装する必要はありそうなので将来のTODOという事で。 >reflect package - reflect - Go Packages Deep Equalなんてのがあった。
フィールドアクセスは、まだ関数定義じゃないletが無いので、それ以前の問題だった。
でもフィールドアクセスの前にUnionとパターンマッチをやりたいな。パターンマッチは軽く文法を見たら結構だるそうだが。
今朝の時点ではやることが多すぎてやる気出なかったが、Recordが出来てしまうとちょっとやる気出てくるな。 Unionは実行時はinterfaceとtype assertionでやれそうな気がするがどうだろう?
昨日考えた方針をとりあえず生成するコードを書いた。 まだ生成したUnionを内部で参照する部分を書いてないので単に生成しただけ、という感じだが。
でもRecordとUnionはだいぶ何をやるべきかは固まってきたので、当初の途方に暮れる感じは乗り越えられたかな。 Unionを終えたあたりでオフサイドルール実装したりletとか関数の中がexpr一つ前提なのを直したりといった整備をしたいが、 あんまり整備をするとセルフホストが遠のくので、悩ましい所。 出来たらセルフホストまで行ったあとに整備をしたい所だが、セルフホストは遠いからなぁ。 とりあえず行ける所まで進めてみるかな?
パーサーも書いてみた。無事動いた。これでレコードとUnionが限定的にだが動くようになった。 あとは簡単なパターンマッチだな。
なんかUnionは生成コードが多いので、随分と出来てきた感が高まってきて、ちゃんと開発しようという気分が高まる。 現在テスト抜くと1300行くらいなのだが、3000行くらいで割と使える所まで行けそうな気がするんだよな。 普通に言語処理系を書くともっとずっとたくさん書かないと使い物になる所まで行かないが、 トランスパイラは下の言語がすでに強力なので、そんなにいろいろ書かなくても使い物になる。 遊びでやるにはいいと思うんだよな。
手抜きだった関数のパースをBlockという概念を持ってきてもうちょっと真面目に実装する。 ただF#のspecのexpressionのexpr exprというルールがパース出来ないじゃん、ってなって、ちょっと適当なルールでのパースになっている。
お、FSharpの方でグラマーっぽいの見つけた。これか。>fsharp/src/Compiler/pars.fsy at 686dcabea0f81eafbf800ec4e7ba6e34580ddf2a · dotnet/fsharp
パターンマッチを実装しようとしたが、オフサイドルールを実装しないと使い物にならないことに気づく。 本格的にやるのは大変そうなので、ある程度決め打ちで手動でハンドル出来ないかなぁ。>手動で適当に処理した
とりあえずAST上でUnionのケースだけパターンマッチを実装してみた。パースを書けば動きそうな雰囲気だが、パーサーを書く気がちょっと湧かずに休憩。
しばらくして気が向いたのでパーサーを書く。結構本格的になってきたなぁ。動いている事は確認。 これでDiscriminated Unionの最低限の実装が確認出来たな。
次は何が必要なのかな?とセルフホストすべく一番小さなftype.goを眺めていると、Sliceとそのイテレーション、そしてifがあれば作れそうだな。 とりあえずSliceだな。
そろそろランタイム用のモジュールを作らないといけない感じだが、ディレクトリの再構成をしなくてはいけなくてやる気が出なかった。 とりあえずディレクトリだけ作り直す。 これまでのトランスパイラはtinyfoというディレクトリにいれる事にする。
これまでGoEvalをunitだったのをtypeパラメータ(genericsの構文)でreturnの型を指定出来るようにする。 これで未完成なものはGoのコードで書いてラップする、という事が出来るようになった。
これまでのトランスパイラのうち置き換え出来そうな部分を探すも、同時にやらないといけない事がいろいろありすぎて手が動かない。 もうちょっと一歩一歩進める方法を考えたい所だが。
という事で、角度を変えて、まだ存在しない機能も使ってしまって、トランスパイラをfolangで書いてみる。 処理系がまだ無いのに言語で書くというのは不思議な感じではあるが、 こんな感じで書けて欲しい、というターゲットを作っているとも言える。
まずは型システムのftype.goをfolangで再実装してみる。 途中まで書いたら、なんとかUnitTest出来そうな感じになったので、まずはここまでをトランスパイル出来るようにtinyfoの実装を進めるか。 この方針はなかなか良い気がするな。まず書きたいコードを書いてそれを実装する。
使ってるがまだない機能は
の2つだな。 スライスはgolangっぽく大括弧を前に置くスタイルにしてみるかな。パースで困ったらFSharp互換にするが、困るまではgolang互換で進めてみる。
of無しのUnionのcase constructorは引数無しなので関数じゃなくて変数になるんだな。 ちょっとその対応をしないといけない事に気づき休憩>実装した
moduleはどうしようかなぁ。packageはディレクトリと紐づいているので、FSharpの粒度よりは大きくなっちゃうんだよな。 ただいっぱいディレクトリ分ければいいのでは、という話もある。 golangの概念と被っているものを入れるのは良くない気もするよな。 せっかく新規に作っているので、この辺はgolangに寄せていきたい気もする。 まずはpackageを使う事にし、困ったらmoduleを検討しよう。
スライスはちゃんとサポートするならgenericsをサポートしてその一部とする方がいいような気もするが、 スライスやmapだけなら特別扱いで良いという話もあるし、golangはそもそもそうなってるんだよな。 とりあえず特別扱いで実装してしまうか。mapはそんなに使ってないのでラップしてしまってもいいかもしれない。
Sliceをサポートした所、レコードのスライスとinterfaceのスライスの区別が内部的にややこしい事になったので、レコードも実体にしてしまう。 どうせ副作用での変更はしないだろうからこれでいいだろう。 ポインタを扱いたい時に困るが、それはgoの型を持ち回るextern的な奴でラップする感じで凌ぎたい。
これでざっと書いたftype.foがトランスパイル出来るはず、と動かしてみるとちょこちょこバグが出てくるがそれらを直していったら無事トランスパイル出来た。 まだftype.goの機能はほとんど入ってないが、最初にUnitTest出来そうな単位が初めてコンパイル出来たという事で記念すべき一歩ではある。
ftype.goを眺めているが、スライス周りのmapとかパイプライン演算子とか整備していけばだいたいFSharpっぽく書けそうだな。 もう2〜3日くらい実装すれば基本的な事は書ける表現力に至れそうな気がする。やっぱfor文回すよりは楽だよなぁ。
次のUnitTestに向けて使う関数を書いてみた。
let fargs (ft:FFunc) =
let l = slice.Length ft.targets
ft.targets |> slice.Take l
このUnitTestを通すのに必要なもの
なんかいっぱい使いすぎだな。たった二行なのになぁ。
ReScriptのarrayはカンマ区切りなんだな。Array & List - ReScript Language Manual
セミコロン区切りにするつもりだったが、カンマ区切りにしようかしら。
スコープとletの実装を終えたのでパッケージアクセスのための外部情報のシンタックスを考える(このページ上の方に書いてある)。 slice.Takeはgenericsが必要な事に気づく。幸いgolangのgenericsは十分な機能を持っているので、そこまで大変でも無い気はしているが。 ようするにFunCallのreturnの型を引数から更新するだけだよな。そのくらいなら出来そうな気もするな。
まぁそろそろその辺に挑む段階に来ているか。
Genericsをサポートしようと途中まで書いたが、やりたいのは定義では無くて外部で定義されたgenericsの関数を呼びたい事だよなぁ、と気づく。 しばらくは自分で定義出来る必要は無いよな。
という事は外部で定義された関数を呼べるよにするのが先か。 一番カンタンなのは以下かな。
package_info slice =
let Take<T> : int->[]T->[]T
ジェネリクスのサポートと外部パッケージ情報の両方を同時にやるのは面倒なので、先にbytes.Bufferのラッパから書く。
package_info buf =
type Buffer
let New: ()->Buffer
let Write: Buffer->string->()
let String: Buffer->string
これをパース出来るようになった。ついでにこのラッパのパッケージをgolang側でも定義する。pkg/bufに置く。 これでfolangから外部のパッケージを呼ぶ手段が確立出来た。
以下のようなコードが無事動くようになった。
package main
import "github.com/karino2/folang/pkg/frt"
import "github.com/karino2/folang/pkg/buf"
let main() =
let bb = buf.New ()
buf.Write bb "hello"
buf.Write bb "world"
let res = buf.String bb
frt.Println res
.foiファイルはmainで先にわたす必要あり。
将来的にはfrtはプレフィクス無しで探すようにしたい気もするが、まずはそういう事はやらずに進める。 なんか一気に完成度が上がったな。
ここまでくればだいぶセルフホストも見えてきたな。ジェネリクスと2項演算を終えたらだいたい書けるんじゃないか?
今日はGenericsの外部呼び出しをやりたいな。sliceをとりあえずサポートしたいので以下か。
package_info slice =
let Length<T>: []T -> int
let Take<T> : int->[]T->[]T
ジェネリクスのインスタンシエートは結局FunCallの中の先頭の関数の型に対してだけ行えば十分…かな? とりあえずその方向でやってみよう。
動いた。これでslice関連が動くように。 せっかくなのでMapも書いてみよう、と思ったら、Mapは高階関数のカッコが無いとうまく動かないな(あれ?カリー化で同型にならないんだっけ?)。 という事でカッコも対応して以下がパース出来るように。
let Map<T, U> : (T->U)->[]T->[]U
なんとなく解決してそれっぽく動くように。
えーと、あとは何が動けば次のUnitTestが試せるんだっけ? 以下を動かしたいので、
let fargs (ft:FFunc) =
let l = slice.Length ft.targets
ft.targets |> slice.Take l
パイプ演算子とレコードのフィールドアクセスと関数の部分適用だな。
部分適用を途中まで書いていたら、generic functionをfunction literalで書く方法が分からないな?となってググったら、出来ないらしい?>go - Generic function, can’t be defined in form of anonymous? - Stack Overflow
手動でカリー化すれば平気かな?
func Take[T any](num int) func([]T) []T {
...
}
take2 := Take(2)
いや、駄目だな。結局、take2の時点でTが解決されていないのは許されていないらしい。 ださいなぁ。
ただ実用上は、型を渡してやればいいのでは?という話はある。
take2 := Take[string](2)
これは許される訳だ。カッコ悪いが、Folangとしては部分適用の時は全部の型を解決する必要がある、というのは一つ妥協点としてはありか? 幸い以下のようなコードでは
ft.targets |> slice.Take l
Takeの部分適用は解決した上でパイプ演算子にわたす事が出来る。 代わりに以下のような事は出来ない。
let part = slice.Take l
part ft.targets
genericな高階関数はFSharpの強力さの根底にあるものなので残念ではあるが、主な用途であるちょっとした書き捨てツールではまぁ許容範囲内か。
partial applyのコードがあったので読んだ。
リフレクションで取って、引数は全部interface{}
にしてしまう。うーん、これでまぁ呼ぶ事は出来るが、結果の型が失われてしまうよなぁ。
これならリフレクションなんて使わなくても同じ事は出来るか。
folang上で型をトラックして最後にキャストする、というのは出来なくはないか。
よし、当面はパイプラインのように部分適用時には解決されている事を前提にする、
解決されないものは将来的にはinterface{}
にしてトラッキングして最後にキャストするようにしたい、
と思いつつそんな日は来ないだろう、という感じでいこう。
もう少し考えてみた。解決してないgenericsのtype parameterを含む部分適用は、ようするにその部分適用を行っているコードをbodyに持つ関数のtype parameterになるのが正しい気がしてきた。 良く考えればFSharpなどもそうなっているよな。
ただそういうのは真面目な型推論を実装する時にやる方が良さそうなので、やはりパイプ演算子だけ動くようにして、それ以外のケースは何もしないで間違ったgoコードを吐いてgoのコンパイルエラーとしておく。
今日はパイプを実装しよう。 パースは最終的にはParsing expressions by precedence climbing - Eli Bendersky’s websiteで実装したいが、とりあえずまだbinopがパイプだけなので簡単に。
ただ以下のケースで
s |> slice.Length
右辺が変数の場合があるが、変数のタイプパラメータの解決という概念を実装してないので実装出来ない事に気づく。
Goとしては何が生成されたらいいんだろう? 別に以下のコードが生成されたら、
frt.Pipe(s, slice.Length)
勝手にタイプパラメータはgoが解決してくれるな。 この時にはexprとしての型が解決されていれば十分なのか。
そして部分適用の場合はもうちょっと頑張らないとまずい。
frt.Pipe(s, func (v T[]) T[] { return slice.Take 2 v })
このTを解決してほしいからだな。 こうして考えるとVarは生成するgoのコードには変化は無くて、exprの型だけ解決すればいいのか。
書いてみたらかなり複雑になってしまったが、とりあえず以下が動きはした。
let main() =
let s = GoEval<[]int> "[]int{5, 6, 7, 8}"
let s2 = s |> slice.Take 2
GoEval "fmt.Printf(\"%v\", s2)"
Takeの戻りが []int
に解決されるのはまぁまぁ頑張ったぜ。
あとはレコードのフィールドアクセスで次のUnitTestが動かせるが、ちょっと燃え尽きたので続きは明日。
生成されたコードを見ていたら、パイプ演算子はインライン化した方が良かったなぁ、という気がしてきた。 まぁ一般的な仕組みとしてこの辺を整備しつつ知ってるbinopは最適化する、というのが順番としては良さそうなので、まぁしばらくこのまま進めてあとで改善しよう。
ちょっと開発日記が長くなってきたので古いのを置く場所を別途作る>Folang過去ログ
今日やりたい事、やった事
importが長いので、folangのpkgに関してはダブルクオート無しでimportする、という事にしよう。 つまり以下の2つは同じ意味にする。
import "github.com/karino2/folang/pkg/frt"
import frt
C系の言語のダブルクオートと角括弧の違いみたいなもんだな。golangにない区別なのがちょっと躊躇するが、まぁいいだろう。
そろそろコメントがほしいな。とりあえずCスタイルのコメントをスペースとして扱おう。>実装した。
次はレコードのフィールドアクセスを実装したいがちょっとやる気が尽きたので休憩。文字列連結も作っておきたいな。
固まった仕様と検討を分離しておく。Discriminated Unionはページを分けたいが、まぁそのうちでいいか。
文字列連結などはFSharpでは+
なのだが、この二項演算はFSharpでも優先度が分かりにくい所。
以下のようなFSharpコードは
hoge a + b
以下のように左が先に評価される。
(hoge a) + b
良く考えればこれは正しいのだけれど、直感的に良く間違えてしまう所。まぁこの辺の仕様はFolangでもそのまま引き継ぐ予定。
レコードアクセスが出来たので実行したら、型定義がmutually recursiveだから駄目だ。
type FType =
| FInt
| FString
| FUnit
| FUnresolved
| FFunc of FuncType // まだ定義されてないFuncType
type FuncType = {targets: []FType} // FuncTypeではFTypeを使う
以前は通ってたが、最近parseTypeですでに定義されているかをチェックするようになったので駄目になった。
FSharpではどうするんだっけ?と思ったらandで定義するのか。
Records in F# - Microsoft Learn
type FType =
| FInt
| FString
| FUnit
| FUnresolved
| FFunc of FuncType
and FuncType = {targets: []FType}
うー、andのパースは結構面倒があるな。2パスにするか、エラーにせずにUnresolved型とかにするかだが、 エラーにしておく方が他の場所では安心なんだよなぁ。 まぁinside type definition的なフラグでparseTypeの挙動を変えるとかやる事は出来るか。幸いtype定義の中では普通の式は来ないからな。 この辺の仕様はさすがに良く出来ているよな、F#。
なるべくセルフホストまでは簡易実装にして、セルフホストが完成してから本格的に作りたいという思いがあるのだが、 セルフホスト出来るくらいまで作るのは結構ちゃんと作る必要があるよなぁ。 だからこそ最初の目標として良いのだろうが。
サンプルからmdを自動生成するコードなどが欲しいが、こういうのこそfolangで書きたいな。
andを実装したら別のmatchのバグが見つかって直したりしている。こんな日もある。で、無事FTypeが定義出来るようになって、 次のUnitTestを通そうとしたら結果が間違っていて、-1しないと駄目な事が判明するも引き算をまだ実装してない。ぐぬぬ。
二項演算を実装しようと思いoperator_plus的なのを探すも見つからず。自分で実装する事に。
The Go Programming Language SpecificationのArithmetic operatorsのあたりを確認して、それっぽいOpPlusやOpMinusをgenericsで書く。
二項演算のパースを少し真面目に書いて、無事UnitTestが通った!
次は必要なのはString.concat
( String (FSharp.Core) - FSharp.Core)なのだが、
golangのパッケージは小文字始まりのpublicメソッドは大文字はじまりなのでstring.Concat
になる。
でもさすがにprimitiveの型名と同じパッケージ名はなぁ。
まぁsくらいつけてstringsかな。すでに使われているパッケージ名ではあるが、folangでそちらを直接使う事はないのでいいだろう。 いや、むしろstrにするか?うーん、どうしよっかなぁ。sliceがちょっと長いなぁ、とは思ってるんだよなぁ。
LengthとConcatくらいしか使わないのでstringsでいいか。
次のターゲットは良く見ると相互再帰が使われている。これってそもそもF#でも変な事しないと書けない感じだった気がするので、考え直すのが正しかった気がするが、どうするんだっけかな?
The “Dependency cycles” Series · F# for Fun and Profit
相互じゃない再帰ならreturnの型のアノテーションをつければ割と簡単なので、returnの型のアノテーションに対応すべきだな(まだしてない)。
パイプで最後がvoidの時が動かない事に気づく。 goのgenericsではunit相当のものはどう書くんだろう?とぐぐったら、どうも別で用意しないといけないらしい。>Using “void” type as a parameterized type in Go generics (version 1.18) or above - Stack Overflow
まぁ別に用意すればいいか。
必要なものを揃えたあとに細々としたバグも直し、無事FTypeToGoのFFuncのケースが動くように。 セルフホストの型の部分で一番複雑な所なので、これが動いたのはftype.goのfolangでの置き換えの山場を超えたと言えそう。
セルフホストの実装を続けていき、次はdictが必要になったが、FSharp的にはこれはsortして比較する方がそれっぽいな。 そしてif then elseがまだない事を思い出すなどした。 でもこの辺はやれば終わる話で難しい事はないな。
sortはdestructiveなのでコピーしないと駄目そうだな。 sort package - sort - Go Packages
sliceのコピー go - Why can’t I duplicate a slice with copy()
? - Stack Overflow
一通り使うものを実装して、ftype.goと同じ内容をfolangで実装してはテストを繰り返し、無事ftype.goが全部実装し終わった! 最初のゴールを無事達成出来た。やったぜ。
ただ次はast.goだがこれは1000行以上あるし、型推論の雑な実装とかがかなり込み入っている。うーん、どうしたもんかなぁ。 やっていってもいいんだが、そろそろちょっとした用途には使えそうなので、先にドッグフードがてら使ってみようかな?という気もする。
いや、少しast.goのコードを見ていたら、型推論のコード以外はfolangで書いてもいい気もしてきた。 型推論はちょっとaddhookすぎるので、実装自体を見直したい気がするので、これをそのまま持っていくのに抵抗があるが、 他はまぁこんなもんだな、という実装になっているので。
という事で次の目標はast.goの型推論以外のコードの実装、にしてみよう。
スライスリテラルが必要になった。 仕様検討の結果、セミコロン区切りにする。
FunCallでIsUnresolvedかどうかの処理を見たりして、これはいまいちなのでそろそろ考えるか、と散歩したりしつつぼんやりと考えてみた。
Genericsについてのメモ - なーんだ、ただの水たまりじゃないか
なんかだいぶ整理されたな。まだ実装が固まるほど理解出来ている感じでは無いが。
ast.goの再実装を進めていて、zipしたくなる。が、タプルが無い。 そうかぁ、こういう系はタプルが要るんだなぁ。 タプルはいろんな所に絡むので無しでセルフホストまで行きたかったが、 どうしてもzipが必要なコードが目の前にあるので仕方ない、実装するか、という気分が高まる。 ただ今日はもう遅いので明日以降だな。
とりあえず要素2つのタプル(pair)を必要な所だけ実装しよう、という気分になる。 destructuringはどうしようかな。無しで済ますかletだけ実装するか。
とりあえずfrt.Fst, frt.Sndで要素を取り出せるようにして、タプル型をなんとなく実装する。とりあえずZipが出来るようにはなった。
ZIpとMapを使っていろいろ実装していたら、文字列リテラルのエスケープの処理が間違っている気がしてきた。 そのままGoに流すべきだよな、これ。でもこれまでのGoEvalはそうでは無い前提になっているな。 たぶんこれまでのGoEvalが間違えている気がするが、ちょっと時間を置いて考えよう。>結局GoEvalの方だけ特殊処理をするように書き直した
もうだいたいセルフホストに必要な機能は揃ってきたとは思うのだが、 再実装する事が結構面倒だよなぁ。
前から考えている事として、外部パッケージと同じように同じパッケージ内のgo側の情報を登録したいな。 パッケージ名をアンダースコアにしたら現在のネームスペースに追加するようにしたい。
前からやろうと思っていたpackage_infoのアンダースコア対応をする。これで同じパッケージ内にwrapper.goとか置いてそこでfolangに足りない機能をgoで補う事が出来るようになった。
&&
を使おうとして未実装な事に気づく。ぐぬぬ。>実装した
なんかセルフホストは目標が遠すぎるので、モチベーションを保つために現時点でもいろいろ使っていきたい気がする。csvplrでも移植しようかと思ったら、FParsecを使っていたり。まぁ大したパースじゃないはずなのでそのくらい自作してもいいんだが、最初の実用的なスクリプトにしては重いなぁ。
やっぱりここまで来たら気合でセルフホスト進めるか、という気になって進める。
フィールドアクセスが多段だと動かないのが面倒になってきたのでidentifierが並ぶケースだけ雑に対応。
大なりと小なりをサポートするのが面倒になってきたので、関数呼び出しにするのでは無くちゃんとBinOpを特別扱いでgolangのネイティブの演算子を吐くように直す。
これでExprToGoが出来た。これはASTからGoのコードを生成する一番大きな所なので、かなり進んだと言える。
パーサーはどうしようかなぁ。ナイーブに書くのは相互再帰とかが出てくるのでfsharp向きじゃないんだよな。 でもパーサーコンビネータ的なのを書くには外部の型のgenericsをサポートする必要がある(現状は関数しかサポートしてない)。
最終的には以下みたいなのを作れるようにしたいが、
Understanding Parser Combinators - F# for fun and profit
それはセルフホストよりあとにやりたいんだよなぁ。
まぁ普通の相互再帰を関数引数に変更する感じでやっていくのは出来なくは無いが(読みづらそうだけど)。
tinyfoの完成度は結構上がってきた気もする。現在テスト無しで3369行。セルフホストまで3000行くらいと思っていたのでだいたい予想通りくらいのサイズ感だな。
あと少しでIRからのトランスパイルが終わりそうだったので終わらせてしまう。 UnionDefの生成が一番の大物だが、これは単純に生成テキスト量が多いだけでロジックは複雑では無いので、 既存のコードを粛々と移植するだけ。
で、無事Stmtからのトランスパイルが終わった!
あとはパーサーと型推論で完成なんだが、この2つをどうするかはまだ決めかねているのだよな。 型推論はイメージしている事はあるのでそれを実装してみたいが、パーサーが無いとテストを用意するのがかったるく、パーサーは決まって無い。 うーむ。
型をfolangで定義しつつtokenizerはgolangで書く事にする。基本的にはtinyfoから持ってくるのだが、folangから使いやすいようにfunctionalなインターフェースに変更する。
パーサーにはスコープがあるが、これがどうもfolangで書きづらい。まだgenericなタイプをサポートしてないので辞書が使えないのだが、 そもそも辞書にputしたりするのもあんまり向いていないんだよなぁ。
とりあえずgolangでスコープ周りを書いてそれを使ってパーサーを書こうとしたが、どうもまだ使えない関数ばかりが実装されていく。 こういう時は進め方が間違っている気がして途中で手が止まる。
すでに完成しているパーサーを持ってこようとするので、途中のレイヤーで実装すべきものが多くなりすぎてこうなってしまうのだよな。 そうでは無くて、まず一つなにかシナリオを通す所から始めるべきか。
一番カンタンなものはなにか。 以下かな。
package main
let ika () =
123
そうだな。次の目標はこれをパースする最低限のパーサーをfolangで書く、にすべきだな。
その前にまずは一行目のpackage文だけパースするのを目標にするか。 なんか見えてきたな。そうしよう。
package文のパースだけ出来た。なんかこれは正しい方向性だな。次は関数定義のletのパースか。
次は関数定義、と思ってパースを書いているが、ついparamsなどのパースを書いていると時間を食ってしまう。 こういうのは良くないなぁ。
そしてレコードの型がついにぶつかる。 いつかはこの日が来るとは思っていたが。
F#としてはPoint.Xなどの表記で曖昧性を解決出来る>Records in F# - Microsoft Learn
これを実装する必要がありそう。そもそもレコード型の生成で型名を書かないのはおかしい気はするけどな。>実装した
paramsのパースはまぁまぁ動くように。このくらい書くとパーサーの書き方はだいたいはっきりしてきたな。 最後の引数をParseStateにする事で、なんかパーサーコンビネータっぽくなってきた。
タプルをいちいち取り出すのが辛くなってきたので、destructuringをペアの時だけ雑に実装。よしよし。
そして無事に以下がパース出来た!
let hoge () =
123
ただ今の所、パーサーはgolangで書く方が楽だな。パーサーをいちいち持ち回るのがかったるい。 普通パーサーコンビネータだとvalueをそのまま素通しするようなパイプ演算子みたいなのが提供されるのが普通だが、 そういうのが無いのでいちいち両者を持ち回る処理を書かないといけないんだよな。 書けなくは無いがgoの方が楽。
やらなくてはいけないことが発散していてやる気が出ないので、とりあえず以下を通すだけに集中しよう。
package main
import "fmt"
let hello (msg:string) =
GoEval "fmt.Printf(\"Hello %s\\n\", msg)"
let main () =
hello "World"
その為には後回しにしていたスコープ周りを追加する必要がある。 ここで手が動かないのはgenericsも考えたいと思ってしまうからだよなぁ。
一応動いた。
このまま完成まで行けるとは思うんだが、ちょっとダレて来たなぁ。 セルフホストは同じものを再実装する必要があるから仕方ない面はあるが。
ここまで実装出来ている時点でtinyfoはかなり実用的だと思うのだが、どうせなら推論を割とちゃんと実装したい気もしていて、 その為にはセルフホストでfolangで書き直してからやりたいという思いがある。 基本的なスクリプトの内容が大きく変わる事が終わる所までは進めたいんだよなぁ。
推論の所を書くのは結構やってみたい所なのでそこまで行けばやる気は出るんだが、そこまでが長い。 まぁあとはパーサーだけなのであと一歩ではあるんだが。 ParseStateを持ち回るのがかったるいんだよなぁ。ジェネリクス周りが弱いからなぁ。
追記:この方針で解決したので過去ログに移した
だいたいはパーサーコンビネータのようなものを使うのがこの界隈では良くやられる事だが、 型の方のジェネリクスはまだ対応してないのと、パターンマッチがまだ弱いので、そのままでは作れない。
個人的には副作用が大きい所は無理せずにgolangで書いて、それをラップしたい気分ではいる。 トークナイザを、パーサーコンビネータみたいに次のステートを作って返す感じの実装にしたい。 そういう、将来F#で書き直しても良さそうな感じの実装が出来たらそれで進めたいとは思う。 だが、F#で手書きでパーサー書いた経験が無いので、完成形が見えていない。
という事でここにいろいろメモとかを残しておく。
ocamlのhand writing parserでググってこんなのを発見>Good example of handwritten Lexer + Recursive Descent Parser? - Learning - OCaml
最後に貼られているリンクの実装はだいたいやりたい事ではある。 OCaml scanner adapted from the Crafting Interpreters book
ただいろんなパターンマッチとwith式が使われているので、このままをサポートするのは厳しいな。 こういうロジックはgoの方でやって、でも型の定義はfolangの方でやる、という感じに出来ないだろうか?
そもそも現在のgolangのtokenizerの実装があるので、あれを移植してみて無理な所を見てみるかなぁ。
とりあえずトークンのスキャンをgolangの側で書いて、トークナイザはfolang側で書くという方針でやってみる。
過去ログを眺めていたら、次は以下を動かしていた。
type hoge = {X: string; Y: string}
let ika () =
{X="abc"; Y="def"}
これはなかなか手頃だな。さすが前回の自分。という事でこれの対応をやろう。
そこそこめんどくさかったが、無事トランスパイル出来た。 骨組みはだいたい出来たかな。
う、package_infoの中のコメントがうまく処理出来てない。明日直そう。
ジェネリクスのサポートが弱いのでfolangでパーサーコンビネータっぽい事が出来ないのでいろいろ面倒なのだが、 golangの方でジェネリクスのutilityを整備してそれを呼ぶなら結構いろいろ出来るのでは?と気付きやってみる。
func withPs[T any](ps ParseState, v T) frt.Tuple2[ParseState, T] {
return frt.NewTuple2(ps, v)
}
withPsでpsを先に作っておいて値が出来たあとに結果を返す、みたいな時にパイプラインで一気に出来るようになった。
let (ps3, rest) = psConsume SEMICOLON ps2 |> parseFieldInitializers parseE
slice.Prepend nep rest |> withPs ps3
今この説明を書いていて、値を加工する関数を渡す方が関数型っぽいな、と思ったがまぁいい。
さらに関数の方を先に進める以下のようなものを作った。
func Thr[T any](fn func(ParseState) ParseState, prev frt.Tuple2[ParseState, T]) frt.Tuple2[ParseState, T] {
p, e := frt.Destr(prev)
return frt.NewTuple2(fn(p), e)
}
これで値を返したあとにEOLをskipする、みたいな事が書けるようになった。
let (ps2, neps) = psConsume LBRACE ps |> parseFieldInitializers parseE |> Thr (psConsume RBRACE)
これはなかなか関数型っぽいな。 やはりThrPとThrEを作る方がそれっぽいか。 そもそもにこれはParseStateには依存してないよなぁ。
本来は以下が正しいか。
func Cnv1[T any, U any](fn func(T) T, prev frt.Tuple2[T, U]) frt.Tuple2[T, U] {
t, u := frt.Destr(prev)
return frt.NewTuple2(fn(t), u)
}
func Cnv2[T any, U any](fn func(U) U, prev frt.Tuple2[T, U]) frt.Tuple2[T, U] {
t, u := frt.Destr(prev)
return frt.NewTuple2(t, fn(u))
}
これならwithPsもいらなかったのでは感。せっかくなのでこう直しておくか。>サポートしてないinferenceが必要になったのでTだけParseStateにした。
要素は0オリジンでCnv0とCnv1の方が正しい気もしてきたが、Cnv1で右側というのもちょっと分かりにくいよな。 CnvLとCnvRか。
func CnvL[U any](fn func(ParseState) ParseState, prev frt.Tuple2[ParseState, U]) frt.Tuple2[ParseState, U] {
t, u := frt.Destr(prev)
return frt.NewTuple2(fn(t), u)
}
func CnvR[T any, U any](fn func(T) U, prev frt.Tuple2[ParseState, T]) frt.Tuple2[ParseState, U] {
t, u := frt.Destr(prev)
return frt.NewTuple2(t, fn(u))
}
これでいいか。
これを使うと、以下みたいなコードが
let parsePackage (ps:ParseState) =
let ps2 = psConsume PACKAGE ps
let pname = psIdentName ps2
let ps3 = psNextNOL ps2
let pkg = Package pname
(ps3, pkg)
以下のように直せる。(psIdentNameNxLとかいうのが増えているがこれは大した事無い)
let parsePackage (ps:ParseState) =
psConsume PACKAGE ps
|> psIdentNameNxL
|> CnvR Package
だいぶ面倒が減ってきたな。パーサー書くのが憂鬱では無くなってきた。いいね。
Unionの実装まで進めた。結構面倒な所だが、stmt_to_go.foの方で7割くらい実装済みなのでこちらはそこまで大変でもなかった。
次はmatchの実装だが、これは逆に思ったより面倒。というよりも、これまで適当に済ませてきたblockとかをどうするかという問題に直面して手が止まったという感じか。
Unionとmatchが結構大物で最初の実装でも割と大変だった所なので、これが終わればセルフホストもだいぶ見えてくる感じに思う。
息抜きに今後の見通しを考える。 とりあえずセルフホストをやったあとに、型推論をちゃんとやりたい。というか関数を基本アノテーション無しで定義するようにしたい。 コードがだいぶ変わるので。 推論前提のコードに変えたあとにアナウンスしたいな。
1/13に作り始めたので今日でだいたい一ヶ月か。意外と一ヶ月で出来るものだな。 正直セルフホストをするのでなければもう使っていける段階に来ているとは思うのだけれど、 セルフホストはドッグフードとしては強力なので機能セットがかなりいい感じになるというメリットを実感している。 いいものにするのに役に立ってるな、と思うので、このままセルフホストを目指して進めていきたい。
match終わった!テストをいろいろtinyfoから持ってきて未実装部分を潰していく。だいぶ前に進んだ。
次はSlice、という所まで進めて休憩。Sliceまでは細々とした事はあるけれど特に詰まる事は無い。 Sliceが終わると外部パッケージ対応で型推論に入る事になる。 ようやくセルフホストの再実装でやりたい所に辿り着けそうだ。
細々とした実装漏れはたまにあるけれど、tinyfoはもうだいたい完成かな。最近はtinyfo側を直す機会も随分と稀になた。 セルフホストを実装するのに必要な機能はだいたい入った気がする。 tinyfoって現在の行数はどんなもんだろう?
tinyfo % wc *go
1344 3805 30220 ast.go
238 612 4633 ftype.go
42 103 755 main.go
1873 4827 35995 parser.go
1146 2840 18671 parser_test.go
37 82 554 transpiler.go
214 431 3739 transpiler_test.go
4894 12700 94567 total
tinyfo % wc *test.go
1146 2840 18671 parser_test.go
214 431 3739 transpiler_test.go
1360 3271 22410 total
tinyfo % echo "4894-1360" | bc
3534
テスト抜きで3500行くらい。3000行くらいで割と使える所までいきそう、という当初の予想は割と正しかったな。
Sliceも無事終了。
夜に興が乗ったのでpackage_infoを一通り動かす。 これは半分型推論の構造を考えながらやる必要があるのでそのまま持ってくるのとは違う実装になるのだが、 ある程度まで進めないと型推論を考える所まで行かないのでgenerics無しのpackage_infoを一通り動かす所まで今日のうちに実装してしまう事に。
これで推論の実装をする準備は整った。
型推論のコードを書こうと思ったが、パーサーのコードに入れるものでも無いよなぁ、と思い、 そもそもパーサーのコードに関係無いハンドラ系のコードも入ってしまっているのでファイルを分離して整理したりする。 だいぶパーサーのコードは見通しが良くなった。
そのあと型推論を実装するにあたり、 トップレベルのStmtは特別扱いが多いので別の型にしたり、必要な情報が足りてないので追加したりと型まわりのリファクタリングを進めつつ考える。 こういうのはtype first development的な良さがあるな。
とりあえずイコールの関係のリストを作るまではまぁまぁ真面目にやった。 このあとに解決する所は今は簡単なケースしか動かない作りになっている(推移律が働かない)。
辞書を作ったあとのコードを整理してもう少し一般化しておく。 周辺を整備しておかないと複雑な問題に挑む気が起こらないので。
丸一日掛かってしまったが、だいぶしっかりしたコードになった。tinyfoでいい加減に済ましていた結果煩雑になってしまった所だったが、 無事リベンジ出来たかな。
あとは推論のコードを書くだけだ。 これが完成したらセルフホスト版がようやくtinyfoを越える部分が出てくる。 セルフホスト完成もだいぶ見えてきたな。
ここ数日ずっと気になっていた型推論のコードを無事実装出来た。 頭にもやもやあるのを書き出してだいぶスッキリしたな。 まだformal type parameterにつけかえる所は書いていないが、 tinyfoではそういうコードは未対応なのでセルフホストには必要無いはず。 tinyfoではうまく解決出来ない引数の関数がtype variableを持つケースもちゃんと解決されるはずで、 気分が良い。そうそう、こう実装したかったんだよな、みたいな。
tinyfoのテストを持ってきて一通りバグを潰す。これで推論はtinyfoと同程度には動いてそうだ。
あとは二項演算を実装すれば大きいのはほぼ完成だな。
気が向いたので二項演算も実装してしまう事に。関連テストをいろいろ持ってきてバグや実装漏れを潰す。だいぶ実装進んだな。
さらに気が向いたのでif式も実装。ここまで来たら最後までやってしまおうかと続けてみたが、さすがに途中で燃え尽きた。
残りのToDoを書き出しておく。
andの方は途中の型を一時的なTypeVarにして終わったあとに置き換える必要があるので、ちょっと最後にえいっとやるには重かった。 ただUnitTestは残り一つ。letの型指定はUnit Testサボってたらしい。実装する時に追加しておこう。
あと半日程度の作業でセルフホストにチャレンジという所までは行く。 バグはいろいろ出てくるだろうが、だいたいはgolang実装と同じ動きなので見比べれば多くはすぐに片付くだろう。
もうここまで来たら細々としたのは片付けてしまおう、という事でandのサポートをする。 そのままの勢いで再帰呼び出しも直し、main関数も書く。 サンプルは全部コンパイル出来た。
ただセルフホストを目指してコンパイルしたら途中でコケるな。 それはそうか。
ここからはバグfixだが、まずは体制を整えよう。
推論をルートの関数定義の所でやっていたが、これだと以下のようなコードで
let hello (h:Holder) =
let fr = h.f1 |> Head
fr.f3
frの型をfr.f3の時点で解決出来ていない、という事になってしまった。 Headの時点でTypeVarを割り当てるのは正しいt思うが、パイプ演算子の時点で解決出来るのなら解決すべきだよなぁ。 それぞれの時点で解決を試みて、最後まで解決出来なかったものだけをルートの関数定義の所でどうにかすべきか。
すぐには片付か無さそうなので今日はここまでかな。
少し考えてみた内容をブログにしてみた。> フィールドアクセスの型解決 - なーんだ、ただの水たまりじゃないか
フィールドアクセス型を作ってみたら今度はスライスとのマッチでうまく行かないケースが出てくる。 ちょっと実装を進める前に時間を置いて考えてみるか。
とりあえず頭の中にもやもやしていた実装は一通り終わり、ここからはまた新しく考えないといけない段階に来た気がする。
こういう本質的な問題を複雑に解決する前に、 もうちょっと前に進むのに必要な所を考えたいよな。 Expr単位で分かっているものを解決していけばこういう問題はあまり発生しないのだから、 そういう方向で難しい問題が発生しにくくなるように頑張った方がいい気もする。
本質的には同じ問題を作る事はいつも出来るはずだが、 実用上あまり出会わないケースならサポートしない、でいい訳で。
型推論は結局、解決出来なければアノテーションつけさせればいいのだから。
一通りいろいろ試してみて、ぐちゃぐちゃになって諦めた。 やはり本質的な解決の前に多くのケースが解決されるようなアドホックなもので前に進もう。
とりあえずパーサーの半分くらいまではトランスパイル出来たので、残り600行くらいだな。結果が動くかはわからんが。
ローカルのinferを実装してとりあえずletの右辺だけやるようにしたら十分だった。 以下みたいにletを挟まずにgenericな関数を呼んでmatchをするケースでは多分駄目だが、
match Head es with
| EInt i ->
...
そういうコードはなかった模様。まぁいいだろう。
その後ぶつかった細々としたバグを直して、ついにセルフホスト完成! tinyfoで作ったfcでfcをトランスパイルして、そのfcでfcをトランスパイル出来る所までは確認。
少し様子を見て問題無さそうならタグを打って最初のバージョンとしよう。 ここからはfolangで書かれたfcでfolangを開発していく。
今後のToDo
上2つは大きいものでは無いのだが、folangで書かれるコードのスタイルが大きく変わる所なので、 アナウンスの前にそこまではやっておきたい。
現状のgoで書かれtる所とfoで書かれている所の比率は以下みたいな感じ。
fc % wc wrapper.go
746 2247 16572 wrapper.go
fc % wc *.fo
86 402 2609 ast.fo
320 1214 8687 expr_to_go.fo
100 375 2612 expr_to_type.fo
123 488 3394 ftype.fo
501 1794 13761 infer.fo
66 217 1623 main.fo
596 1937 15662 parse_state.fo
1059 4147 31274 parser.fo
247 856 5781 stmt_to_go.fo
93 267 1593 tokenizer.fo
3191 11697 86996 total
746行のうち、トークナイザが400行弱で、それ以外はほとんど辞書関連。 辞書をサポート出来るようにすればfoで再実装出来るので、そこまではやりたいな。
夜に気が向いたのでパラメータのinferenceと解決してない時にtype parameterに昇格してgenericな関数を生成するように。 以下のようなhogeという関数があった時に
let hoge a =
slice.Head a
let main () =
let b = [1; 2; 3]
let c = hoge b
frt.Printf1 "%d\n" c
以下のような関数が生成される。
func hoge[T0 any](a []T0) T0 {
return slice.Head(a)
}
このようにT0が勝手に振られるようになった。そしてパラメータのinerenceもサポートされた結果、引数にいちいち型アノテーションを書かなくても良くなった。
あとはgenericな型の対応だけだ。
必要な事を考える。現状2つの事が出来ていない。
スライスは以下みたいに書きたい訳だが
package_info slice =
let New<T>: ()->T[]
このTは引数から推測は出来ないので、型パラメータを渡せる必要がある。
let ika () =
let s = slice.New<string> ()
...
これは N < 3 > a
のような二項演算との区別に気をつけてパースする必要があるが、まぁパースは出来るだろう。
現在はgenericな関数はtype factoryとして登録されて、参照されるとType Variableをassignしているが、その時に引数が渡されたらそれを使うようにする処理が必要なのだな。
これはやれば出来そうな気はする。
そしてDictは以下みたいな感じか。
package_info dict =
type Dict<K, V>
let New<K, V>: ()->Dict<K, V>
Newの戻りの型がスライスなどと同様のcompositeな型で、タイプパラメータのリストを持つようにしてあればまぁ行けそうか。 スライスと割と似ているのでスライスの実装を真似すれば良さそうだな。
Newの方を実装した。これでGoEvalしてた空のスライスの作成がfolangで出来るようになった。
自分的に最後のToDoだったDictのサポートのためのgeneric 型を、外部のパッケージの時のみ対応。 Folang内での定義はまだ出来ないが、それはおいおい。
そしてDictを実装する。FSharpとしては型名はMapがimmutableなものとしては使われるが、 一方IDictionaryを作るキーワードはdictで、この辺は微妙なので無理に揃えるのはやめてdictで。
folangで書けなかったものがいろいろ書けるようになったので、wrapper.goの中身をfolangで再実装していく。 なかなか楽しい。
scopeを実装したら以下は無理と言われた。
type MyScope struct {
// dictいろいろ
Parent MyScope
}
そうか。ポインタじゃないと駄目か。
ということでgolang側でポインタでそこだけ書いて、他をfolangで再実装する。 wrapper.goは514行に。foが3649行なので、だいたいfolangで実装出来たと言っていいんじゃないか。
型推論はかなりちゃんと動いていて、だいぶ型指定無しで書けるようになった。
現状dictはdict.Dictとして書かないといけないが、Dictでいいようにしたいなぁ、という気持ちがある。 一方でopenとかをちゃんとサポートする方がいいという話もある。うーん。
幾つか同じロジックを別の型用に作っていたのをgeneric版に書き直したりする。
ここまでで当初思っていた、リリースまでにやろうと思っていた事は出来たかな。 書いているコードもだいぶいい感じになってきた。
そろそろドキュメントを書く時期に来たかもしれない。
スタックトレースが深すぎて辛いのでFoldなどを実装してそれを使うようにする。 いい感じになった。
サンプルからmdを生成するツールを書いたりして見つかったバグを直したりしていた。 生成したmdはこちら。
folang/samples/README.md at main · karino2/folang
生成したmdを見ていたら古い書き方が多かったので直したり。 なんかすごい完成度上がったように見えるな。よしよし。
次はGetting Startedを書くかな。
とりあえずGetting Startedから書き始める。
folang/docs/tutorials/1_GettingStarted_ja.md at main · karino2/folang
とりあえず日本語で書いて、自動翻訳して手直ししよう、という作戦。
Google翻訳は思った以上に有能なので、日本語メインで文書を書く事に決定する。 このWIkiに書いてある仕様系も移していきたいな。
とりあえずチュートリアルはだいたい書き終えた。 ただ他にも必要なものがいろいろあるなぁ。 正直この整備は永遠に終わらないので、 適当な所で切り上げる必要がある。
一旦ここまで書いたものをインデックスつけたりGoogle翻訳通して手直ししたり、 という整備を進めるかなぁ。
とりあえず最初のマイルストーンを決めよう。
このくらいを最初の目標とするか。
ここに書いていた内容もgithubのmdに移していく。 google翻訳で英語版も用意して軽く見直していく。見直しは大変だが、それほど直す必要がないので費用対効果の悪い所だよなぁ。
一通り揃ったのでブログとredditに投稿。
F#ライクな関数型言語のGoへのトランスパイラ、Folangを開発した - なーんだ、ただの水たまりじゃないか
まぁそこまで第三者が試せる感じでは無いが。
気が向いたので関数リテラル(ラムダ式)を実装してみる。 割と簡単に出来た。
そのまま勢いでショートハンドnotationも実装。いいね。
細々としたのは残っているが、大きいのはRecordのGenericsだよなぁ。 逆にそれさえ実装すればパーサーコンビネータをはじめとした多くのものが実装出来るようになる気がする。 ただ、もうちょっと使ってからそういうのは手を出したい気もするんだよな。
Golang側のgenericsを触ってみて、やれば出来そうだな、という感触は得る。仕様検討の方に移動。
軽い気持ちでちょっとinferenceのバグを直そうとしたら相互再帰型がめちゃバグってて半日掛かってしまった。まぁこんな日もある。 Unionはポインタにするしか無い、という結論になり、goのwrapperでptr型を書くなどした。
次の大きなマイルストーンとしては、以下あたりを終えた所としようかな。
割とそれぞれそんな大変では無いと思っているが、全部足せばそれなりになってしまうので、 中期の目標として考えた方がいいな、と思った。
とりあえず次はパースエラー直すかな。直す前に、とりあえずスタックトレースを消した。 そろそろ邪魔なので。
なんか寒くて他の事をやる気がおきなかったのでエラーメッセージを直してしまう。
夜に気が向いたのでRecordのジェネリクスに着手したが、色々理解が足りてない事が露呈したのでまた明日。
一晩たってだいたい方針が固まったので朝飯を食べつつ実装。無事動く。ただレコードはそんなに使う所無いんだよな。 Unionが本丸。
F# で書いたツールをいろいろ移植したいと思い、photinoの代わりはなんか無いかなぁ、と調べてて、Introduction - Wails というのを見つける。 こういうの試すなら文字列リテラルをもっとリッチにしたいな。
グローバル変数のメモ。
以下はNG (“syntax error: non-declaration statement outside function body”)
func AddTwo(a string, b string) string {
return a+b
}
g_str := AddTwo("hoge", "ika")
以下はOK。
func AddTwo(a string, b string) string {
return a+b
}
var g_str = AddTwo("hoge", "ika")
ちなみに右辺がこういう関数とかだとconstはNG。Folang的にはvarになればいいのかな。
夜に気が向いたのでstring interpolationを実装しておく。こういうパースはGolangでやるのがFolang流、ということでGolangで書いてコード生成。割とあっさり出来た。ドル始まり。以下みたいなの。
$"This is var a: {a}, var b is {b}."
raw stringはGolangに合わせてバッククオートにしたいな。こちらにもドルもつけられるようにしよう。 この辺やったらWebView系の何かで小物を作ってみたいな。
簡単に対応出来そうなので朝起きて軽くbacktickのrawstring対応。 ついでにrawstringのstring interpolationも対応しておく。
これでwebview使ったアプリを書く準備は出来たかな。