scalaでmuscle assert的なライブラリの試作
Scalaでdogというテスティングフレームワークを以前から作ってみているわけですが、こいつでPersimmon.MuscleAssertみたいなことはできるのかなと思い作……ろうとして放置していたものを少し手直しして動くようにしました。
https://github.com/scala-kennel/hound
Diffの実装自体はただのshapelessのようだ。
初期はhttps://github.com/xdotai/diffがよさそうに見えたのでこのライブラリを使っていたのですが、所々使いづらかったりあまり更新頻度高くなさそうだったりScala.js対応されそうになかったりしたので「Diffくらい作れるでしょ」という軽い気持ちで自作に走りました。
なお実装が雑なのでcase classの型が異なる時に型名しか表示されないです。 あと、shapelessを使いサボった関係で、そもそもprimitive type + case classくらいしか表示できない問題があります……Scalaでリフレクションを頑張る体力がないです……。
Stringの個別diffはライブラリがぱっと探せなかったので非対応です。 よさげなライブラリがあれば教えて下さい(Javaのものでも構いません)。
assertion時にオブジェクトのdiffを取得するのは、たぶん有名どころのテスティングフレームワークでも実装できると思います。 が、まぁ需要はなさそうですし私は作る予定はないです。 ScalaTestにはpower assertあるので、基本はそっちで満足する人が多いんじゃないかなぁと。
最小のコンピュテーション式
メモ。
使う規則
T(e;, V, C, q) = C(e;b.Zero())
この規則がvalidなコンピュテーション式を作れるはず。
Zero
メソッドのみを用意すれば良いのでBuilderの実装も最小限なはず?*1
コード
// 定義 type A() = member x.Zero()=() let a = A() // 実際に試す a { () }
F# 4.0のfsiで確認。
- ビルダーのクラス名に制限はない
- 束縛名にも制限はない
なので一文字でも問題なく判別可能です。
公開後の追記
最小のコンピュテーション式
— ぜくる (@zecl) 2016年10月24日
type A() = member https://t.co/aX9g8Fu0HT()=x
コレ説(´・_・`)
単にビルダーインスタンスを返すだけなのでこっちが正しいですね。
自分でx
にしておきながら気付いてなかったです…失礼しました。
zeclさん、指摘ありがとうございました。
番外編
バッククオート2つで囲めばIdentifierにできるので、(たぶん)絵文字も可能です。
type ``🍣``() = member x.Zero()=() let ``🍣`` = ``🍣``() ``🍣`` { () }
表示されない場合は環境の問題でしょう。
*1:と思っていたらミスしてました。追記部分を読んでください
.NET Fringe Japan 2016の個人的な振り返り
dotnetfringe-japan.connpass.com
運営及び発表者として参加しました。
運営といっても、勉強会運営に慣れている方が多かったのでやること・やらないことが初期から明確だった気がします。 当日の懇親会周りの運営を手伝えなかったのは申し訳なく思いつつ……。
発表内容
not_fsharp_deep_dive.md · GitHub
発表内容はぎりぎりまで悩みつつ、気がつけば難産だったらしいkekyoさんとチキンレースを繰り広げていました。
最初はコンピュテーション式のコンパイラ側の話にしようと考えていましたが、朝からそんな話を聞かされるのはつらいだろうと判断して早々に却下。 次にFSharp.Compiler.Serviceに挑戦しようかな、と考えましたが、別の発表でRoslynがでてきそうな気配があったのでこれも見送り。 では自分らしくどういうライブラリを作ったかについて話すか……と思いつつ、いやいやnueccさんの発表があるからなぁ、みたいな。
そうした紆余曲折があった結果、F# がどういう機能を持ち、持たず、その上で具体的に何が作れるのかについてしゃべることにしました。 が、今考えるとFSDNの話は削ってF#とC#関係について話した方が良かった感ありますね。
F#は今の所、.NET Frameworkや.NET Core、C#とは縁を切れない関係です。 おそらく今後もF#は何かしらの影響を与え、そして与えられる立場であることでしょう。 そういう意味で、F#のRFC機能について紹介しました。 RFCはディープではないはず……と思っていたけど、今考えたら「策定段階の企画書読む」って十分ディープでしたね……。
あと、発表の途中で".NET Framework 2.0ではTask
の独自実装が〜"と口走ったのは完全に間違いで、正しくはCancellationTokenSource
です。
大変失礼しました。
全体
今回の登壇者が同じ場所に揃う機会、ないのではないでしょうか。 Partitionの話とか、なかなか聞けないので面白かったです(なお理解度は怪しいorz)。 あとは、本当にマルチプラットフォームだなーという謎の気持ちが大きかったですね。
時間が長丁場だったのは反省点か……とはいえ、もう少し大きな会場で2部屋使えれば解決できるとは思いますが。 そういう意味では、継続するには協力者を増やす必要がありますね。 まぁ、来年のことは来年考えましょう(とか言っているとすぐ来年になる)。
終わりに
異端と呼ばれているイベントに登壇する機会をいただけて、かなり満足しました。
ueberauth_qiitaとueberauth_hatenaを作った
久々にElixirネタ。
といっても表題がすべてを表していますが……。
https://github.com/pocketberserker/ueberauth_qiita
https://github.com/pocketberserker/ueberauth_hatena
ueberauthはElixir向けの認証ライブラリで、RubyのOmniauthに強い影響を受けているらしいです。 TwitterやFacebookなど主要なサービスは一通り実装されているので結構便利です。
とはいえ、さすがに日本向けサービスの実装はなかったので、仕組みを調べるついでにQiitaとはてな用のものを作りました。
ueberauth_qiitaはhexにpublish済みですが、ueberauth_hatenaはとある依存ライブラリをscm形式で依存させている関係でpublishできていません (昔はそれでもhexにpublishできていたのだが、仕様が変わった?)
基本的にはueberauth_facebookやueberauth_twitterと同様の実装になっています。 まぁ、サービスごとに微妙に挙動が異なるので辛い気持ちになりましたが……「もう少し統一感だしてくれー」と叫びたくなりました。
特にこれといった技術的解説点もないので以上。
TypeProviderでFizzBuzzを取得可能な自然数型を生成する
コンパイル時に生成できますね、というだけの話です。
準備: FSharp.TypeProviders.StarterPackのインストール
NuGetからインストールしてください。
注意点としては、ファイルの定義順序がそのままだとコンパイルできない可能性があることでしょうか。
- ProvidedTypes.fsi
- ProvidedTypes.fs
- DebugProvidedTypes.fs
でコンパイルできるはず。
型を生成してFizzBuzzを取得できるようにする
https://github.com/pocketberserker/MLStudy/commit/9a0a2795d5f8915cab89a5badf65d3abe17b1cf1
FizzBuzzは1から100まで
と範囲が明確に決まっていますが、それでは面白くないので任意の範囲でとれるようにしましょう。
ProvidedStaticParameter
でTypeProvider利用時に引数を渡せるようになるとかなんとか。
定義したProvidedStaticParameter群はProvidedTypeDefinition#DefineStaticParameters
に渡すことで登録でき、第2引数で生成の操作を記述できます。
λ式内でやっていることは簡単です。
使う側ではlet inline dump (value: ^n) = (^n: (member FizzBuzz: string) value) |> printfn "%s"
という風に静的に解決された型パラメータを用いてFuzzBuzz
を持つ値を出力できるようにします。
足し算がしたい
https://github.com/pocketberserker/MLStudy/commit/aee21c03a3d7e804c48e9c2fc13655c459645e40
数値なのに足し算できないのはおかしい、ということで足し算できるようにします。
- 数値と型のマッピングをもっておく
- 足し算で得た数値の型が存在するなら
op_Addition
メソッドを定義、そうでないなら何もしない
こう改良することで、生成される範囲の自然数型で足し算ができます。 もちろんFizzBuzzも表示できます。
余談
ここで生成した各自然数型は共通するインターフェースを持ちません。 足し算程度ならインターフェースを用意しなくてもオーバーロードでごり押しできます。
発展
この状態ではリストを取得できません。 HListを用いれば良いはずですが、これに関しては読者の課題とします。
Twitterで流れてきたListReaderコンピュテーション式の解説
先日、Twitterで id:n7shi さんが面白いコードを投下していた。
コンピュテーション式のビルダーは1回しか書いたことがないので、簡単なものでも苦戦した。用途を限定してリストを順に読む例(モナドではない)。 https://t.co/Zfq0M9vm0n
— 七誌 (@7shi) June 29, 2016
これに対しての私のreplyが以下。
@7shi @callmekohei 面白そうだったのでいじってみました https://t.co/yg7dLSHKs4
— ナゲット・もみあげ (@pocketberserker) June 29, 2016
これらのコンピュテーション式はちょっとわかりづらいのでちょっとした解説を試みる。
7shiさん版ListReaderBuilder
7shiさんのコードに少し手を加えたものが以下のコードである。 元コードとの挙動に差はない。
type ListReaderBuilder() = member __.Bind(_, f) = function | x::xs -> f x xs | [] -> () member __.Zero() = fun _ -> () let listReader = ListReaderBuilder() let test = listReader { let! a = () in printfn "%d" a let! a = () in printfn "%d" a } test [1;2;3] printfn "---" test [4]
このコードの出力は以下のようになる。
1 2 --- 4
このビルダーで使われる変換規則は以下の通り。
T(let! p = e in ce, V, C, q) = T(ce, V ⊕ var(p), λv.C(b.Bind(src(e),fun p -> v), q)
T(do e in ce, V, C, q) = T(ce, V, v.C(e; v), q)
T(e;, V, C, q) = C(e;b.Zero())
test
を展開すると以下のようになるだろう(実際はもっと異なるかもしれないが、私はコンパイラではないのでわからない)。
// val test: int list -> unit let test = // let! a = () in ... の展開 listReader.Bind( (), fun a -> // do printfn "%d" a in ... の展開 printfn "%d" a // let! a = () in ... の展開 listReader.Bind( (), fun a -> // printfn "%d" a; の展開 printfn "%d" a listReader.Zero() ) )
- Bindメソッドのシグネチャは
'T * ('U -> 'U list -> unit) -> ('U list -> unit)
- Zeroメソッドのシグネチャは
unit -> ('a -> unit)
((Bindの型パラメータと異なることを強調したかったのであえて'a
表記にしている)) printfn
部分でa
はintに固定される
ことを考えれば、型に問題はないことに気づくだろう。
考察
面白いコードだが、個人的に気になったことがあった。
let! a = () in ...
がなんだか冗長に見えるのだ。
do! ...
のほうが短くて見やすい気がする。
いじってみた版
type ListReaderBuilder() = member __.Bind(g, f) = function | x::xs -> f (g x) xs | [] -> () member __.Return(_) = fun _ -> () let listReader = ListReaderBuilder() let test = listReader { do! printfn "%d" do! printfn "%d" }
このビルダーで使われる変換規則は以下の通り。
T(let! p = e in ce, V, C, q) = T(ce, V ⊕ var(p), λv.C(b.Bind(src(e),fun p -> v), q)
T(do! e in ce, V, C, q) = T(let! () = e in ce, V, C, q)
T(do! e;, V, C, q) = T(let! () = src(e) in b.Return(), V, C, q)
同じようにtest
を展開してみる。
// val test: int list -> unit let test = // do! printfn "%d" in ... の展開 listReader.Bind( (printfn "%d"), fun () -> // do! printfn "%d"; の展開 listReader.Bind( (printfn "%d"), fun () -> listReader.Return(()) ) )
- Bindメソッドのシグネチャは
('T -> 'U) * ('U -> 'T list -> unit) -> ('T list -> unit)
- Returnメソッドのシグネチャは
'a -> ('b -> unit)
- 最終的に残ったリストはReturnが返すλ式で捨てられる
g x
でprintfn "%d" x
が実行され、その戻り値unit
がf
に渡る
こちらも型は問題ないことがわかるだろう。
やりたいことができるか試す
その後の7shiさんとのやり取りで、以下のようなことがやりたかったというコメントを頂いた。
改変後のコードでも同様の挙動にできるか試してみよう。
ビルダーとaddPerson
のシグネチャの順序、コンピュテーション式を書き換える。
// 変更したコードのみ載せています type ListReaderBuilder() = member __.Bind(g, f) = function | x::xs -> f (g x) xs | [] -> () member __.Return(_) = fun _ -> () ... let addPerson data name = if name <> "" then persons.[name] <- data ... row |> listReader { let! company = id do! addPerson (company, "会長") do! addPerson (company, "社長") do! addPerson (company, "副社長") do! addPerson (company, "専務") } ...
最初の値は他の場所で使いたいのでid
で取得する。
残りはprintfn
の時と同じ要領だ。
実際に試してみると同じ結果を返した。
おわりに
結果は同じでも、ビルダーの定義次第でコンピュテーション式の書き方が変わる。 他言語のマクロのように自由ではない限られた変換で何が表現できるのか、疑問は尽きない。
F# API検索サービス"FSDN"を作りました
公開から少し間が空きましたが、改めて周知ということで。
http://fsdn.azurewebsites.net/
このサービスは現在Azureで運用しています。 運用にあたり、株式会社オンザロード様にスポンサーについていただきました。
私がメンテナンスしていく限りはUNIX、Linux環境でも動くようにしておくつもりです。 ただ、メンテナが変われば方針が変わるかもしれません。
検索対象ライブラリ、アセンブリ追加要望は https://github.com/fsdn-projects/FSDN/issues でお願いします。
FSharpApiSearchにべったり依存しているので、機能によってはそちらを先に拡張しないとFSDN側ではどうにもならない可能性が大きいのです。
技術的なお話はまた今度。