並行並列OCaml5.0
こんにちは、びしょ〜じょです。 これはMeta Languages Advent Calendar 2021の3日目の記事です。 今日は12月3日、冴草きいちゃんの誕生日です。いいね? めでたいです。
本日は、来るOCaml5.0のリリースに先駆けてカンタンに紹介します。
はじめに
OCaml 4.14が4.x系最後のマイナーバージョンとなり、2022年にメジャーアップデートして5.0がリリースされる機運がかなり高まってきました。 5.0ではMulticore OCamlの成果が初めてメインラインにマージされ、これにより並行と並列の2つの目玉機能が入ります。 並行と並列のプリミティブですよ!! モダンですねえ。 だいたいこの辺の話を部分的に深掘ります。
ちなむとopam switch
で並列並行プリミティブの入ったMulticore OCamlが楽しめます。
$ opam switch 4.12.0+domains+effects
以下、このswitchで利用できるOCamlの話をベースにします。
並列 ― Shared-Memory Parallelism
念願の並列プリミティブです。
並列性に関する処理系の内部的な変更に伴い、parallel minor GCも追加されました。
並列計算をおこなわないプログラムでも恩恵が得られそうですね。すばらし。
Domain
というモジュールに並列プリミティブが入っています。
また、domainslib
というMutlicore OCamlチームが提供しているパッケージではこれらをラップしてより利用しやすくなっています。
domainslib
は後述するalgebraic effectsを内部でバッツリ利用しており、読んでみるのも面白いです。
let open Domainslib in
let pool = Task.setup_pool ~num_additional_domains:3 () in
Task.parallel_for ~start:0 ~finish:10 ~body:(Printf.printf "hello, %d\n") pool;;
(* prints:
hello, 10
hello, 9
hello, 8
hello, 7
hello, 6
hello, 2
hello, 1
hello, 5
hello, 0
hello, 4
hello, 3
*)
並行 ― Direct-Style Concurrent Programming via Algebraic Effects
とうとう来たぞ!!!! 2022年もAlgebraic Effects元年や!!
Algebraic effectsについては本ブログで散々こすってきたネタなのでそちらをご覧ください。
OCamlにはLwtやAsyncなどhardly-usedな非同期ライブラリがすでに存在します。
が、algebraic effectsを導入することで、これらをdirect styleで書くことができるようになります。
Lwtはlet
operatorを用意したりAsyncはppxを利用することでdirect-styleを実現できますが、operatorsの入ったモジュールをopen
する必要があったり複数のlet
operatorを利用したいときなどに不便です。
そこでalgebraic effectsを利用することで、let
やppxなどのsyntacticなサポートなしに、direct styleでプログラムをかけます。
JavaScriptでいえばasync
/await
キーワードが入ったような革命ですが、algebraic effectsの場合はcallee側にsyntacticな変更が必要ないのはもちろん、caller側もハンドラにcalleを渡すだけでよい点がsyntacticに軽量です。
こちらもalgebraic effectsを利用したeioというパッケージでFiber
などの頻出データ構造などが使えます。
…とここまでalgebraic effectsについて色々書いたけどOCaml5.0ではsyntactic supportはありません! どいういうこと?
エフェクトの発生はperform @@ Choice (1, 3)
と同じですが、エフェクトの定義はtype _ eff = ..
というextensible variantで表現します。
つまり、Multicore OCamlで書いていたeffect 'a Choice = ('a * 'a) -> 'a
のようなエフェクトはtype 'a eff += Choce : ('a * 'a) -> 'a eff
と書きます。
ハンドラはもうすこしややこしく、try_with
という関数を使います。
try_with (perform @@ Choice (1, 3))
{ effc = fun eff ->
match e with
| Choice (l, r) -> Some (fun k ->
if (Random.int > 1) then continue k l
else continue k r
)
| e -> None }
ハンドルされるかどうかを示すために、各エフェクトのハンドラはoptionで包む必要があります。
ハンドラの取る引数k
は継続ですね。これをcontinue
に渡して実行します。
Syntacticな変更をほどこさなかったのは、OCamlにも将来effect-and-type systemを入れるつもりがあるため、そのときまではsyntaxを確定させたくないからのようです。
ただ、面白いのは上記で利用したtry_with
のパスです。
この関数はEffectHandlers
モジュールの子モジュールDeep
とShallow
にそれぞれ用意されています。
これは名前の通りdeep handlerとshallow handlerが用意されていることです。
deepとshallow2つが用意されているとたいへん便利なのですが、ハンドラのsyntaxをもうちょっと考える必要がでてきます。
ここにもsyntaxの追加を見送った理由がありそうです。
EffectHandlers
モジュールの中身がオモロイんで話しまくってしまいそうなので、シグネチャだけ置いておきます。
discontinue
とかShallow.continue_with
あたりが楽しそう。
utop# #show EffectHandlers;;
module EffectHandlers = EffectHandlers
module EffectHandlers :
sig
type _ eff = ..
external perform : 'a eff -> 'a = "%perform"
module Deep : sig ... end
module Shallow : sig ... end
end
utop# #show EffectHandlers.Deep;;
module Deep = EffectHandlers.Deep
module Deep :
sig
type ('a, 'b) continuation
val continue : ('a, 'b) continuation -> 'a -> 'b
val discontinue : ('a, 'b) continuation -> exn -> 'b
val discontinue_with_backtrace :
('a, 'b) continuation -> exn -> Printexc.raw_backtrace -> 'b
type ('a, 'b) handler = {
retc : 'a -> 'b;
exnc : exn -> 'b;
effc : 'c. 'c EffectHandlers.eff -> (('c, 'b) continuation -> 'b) option;
}
val match_with : ('a -> 'b) -> 'a -> ('b, 'c) handler -> 'c
type 'a effect_handler = {
effc : 'b. 'b EffectHandlers.eff -> (('b, 'a) continuation -> 'a) option;
}
val try_with : ('a -> 'b) -> 'a -> 'b effect_handler -> 'b
external get_callstack :
('a, 'b) continuation -> int -> Printexc.raw_backtrace
= "caml_get_continuation_callstack"
end
utop # #show EffectHandlers.Shallow;;
module Shallow = EffectHandlers.Shallow
module Shallow :
sig
type ('a, 'b) continuation
val fiber : ('a -> 'b) -> ('a, 'b) continuation
type ('a, 'b) handler = {
retc : 'a -> 'b;
exnc : exn -> 'b;
effc : 'c. 'c EffectHandlers.eff -> (('c, 'a) continuation -> 'b) option;
}
val continue_with : ('a, 'b) continuation -> 'a -> ('b, 'c) handler -> 'c
val discontinue_with :
('a, 'b) continuation -> exn -> ('b, 'c) handler -> 'c
val discontinue_with_backtrace :
('a, 'b) continuation ->
exn -> Printexc.raw_backtrace -> ('b, 'c) handler -> 'c
external get_callstack :
('a, 'b) continuation -> int -> Printexc.raw_backtrace
= "caml_get_continuation_callstack"
end
おわりに
ア~~わくわくしてきました、はやくOCaml5.0こいこいこい