こんにちは、びしょ〜じょです。 これはMeta Languages Advent Calendar 2021の3日目の記事です。 今日は12月3日、冴草きいちゃんの誕生日です。いいね? めでたいです。

本日は、来るOCaml5.0のリリースに先駆けてカンタンに紹介します。


はじめに

OCaml 4.14が4.x系最後のマイナーバージョンとなり、2022年にメジャーアップデートして5.0がリリースされる機運がかなり高まってきました。 5.0ではMulticore OCamlの成果が初めてメインラインにマージされ、これにより並行並列の2つの目玉機能が入ります。 並行と並列のプリミティブですよ!! モダンですねえ。 だいたいこの辺の話を部分的に深掘ります。

With the convergence between the multicore and standard runtime across OCaml 4.10.0 to 4.13.0 [6], the development of OCaml multicore has reached a point where further integration into OCaml’s main branch requires fully committing to a switch to OCaml multicore. The OCaml team has decided that the time has come for such a commitment. The new major version, OCaml 5, will be a multicore version of OCaml. Moreover, OCaml 4.14 will be the last minor release of the 4.x series of OCaml. Multicore...
Welcome to the September 2021 Multicore OCaml monthly report! This month’s update along with the previous updates have been compiled by me, @ctk21, @kayceesrk and @shakthimaan. The team has been working over the past few months to finish the last few features necessary to reach feature parity with stock OCaml. We also worked closely with the core OCaml team to develop the timeline for upstreaming Multicore OCaml to stock OCaml, and have now agreed that: OCaml 5.0 will support shared-memory para...
Tutorial on Multicore OCaml parallel programming with domainslib - ocaml-multicore/parallel-programming-in-multicore-ocaml

ちなむと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にはLwtAsyncなど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モジュールの子モジュールDeepShallowにそれぞれ用意されています。 これは名前の通り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こいこいこい