GHCに線形型を導入すると以下のような良い事があるらしい。

リソース安全性: ファイルハンドル、ソケット、DBコネクションのようなリソースについて、これらを提供するAPIの設計者が安全な使用を強制できる。リソース解放後のアクセス、二重解放、解放忘れを防止することができる。

レイテンシ: リソースAPIの実装をうまくやるとoff-heap(GCの対象外)でリソースを確保・解放できる。GC対象が少なくなることによりGCによってプログラムが停止する時間を減らせる。

並列性: 過剰な直列化を強要しない。リソース安全性を保ちつつもできる限り並列化できる。

詳しくはproposal。

この記事では線形型GHCのプロトタイプ実装および線形型ファイル操作ライブラリのプロトタイプ実装を試してみる。 ここで紹介するものが正式リリースで変更されている可能性は大いにある。

なおcabalのnew APIを使うのがマイブームなので今回はstackを使わない方法でやっていく。

GHCのビルド準備

まずGHCのビルドに必要なものをインストールする。

autoconf

automake

ncurses

happy

alex

cabal-install

ghc

このうち、autoconf, automake, ncursesはapt等でインストールできるはず。

ghcはUnix系環境であればghcupで導入するのがやりやすいと思う。 異なるバージョンのghcを切り替えることもできる。 (今回のように独自ビルドしたghcも管理対象に追加できるのかは未確認)

ghcup install 8.6.3 ghcup set 8.6.3 ghcup install-cabal ghcup new-install cabal-install

happy, alexはcabalで入れればよい: cabal new-install happy alex

linear-typesブランチをビルド

GHCのソースはGitHubのミラーから取得するのが速い。

git clone https://github.com/ghc/ghc

linear-typesブランチは別のところで開発されているのでリモートソースに追加してチェックアウトする。 (この記事でのcommit id: 782869e3d1a25b4a84c405be346ef5b9c1fbfc8b)

cd ghc git remote add tweag https://github.com/tweag/ghc.git git fetch tweag linear-types git checkout tweag/linear-types

GHCのgit運用がGitHubとミスマッチを起こしているところがあるので少し手を加える。

git config --global url."git://github.com/ghc/packages-".insteadOf git://github.com/ghc/packages/ git config --global url."http://github.com/ghc/packages-".insteadOf http://github.com/ghc/packages/ git config --global url."https://github.com/ghc/packages-".insteadOf https://github.com/ghc/packages/ git config --global url."ssh://git\@github.com/ghc/packages-".insteadOf ssh://git\@github.com/ghc/packages/ git config --global url."git\@github.com:/ghc/packages-".insteadOf git\@github.com:/ghc/packages/

ビルドする。makeのNは物理CPUコア+1にするのが良いらしい。

./boot ./configure make -j N

ビルドできたら、 ./inplace/bin/ghc-stage2 がよく知るghcコマンドになっている。

実際に試す

やっと線形型GHCを実行する準備ができた。 コードを様々に変えて試すには ghci の :reload や ghcid の変更検知のようなインタラクティブな環境を用意するとやりやすい。

ghci は ./inplace/bin/ghc-stage2 --interactive で起動する。

ghcid なら ghcid --command='/path-to-ghc/ghc/inplace/bin/ghc-stage2 --interactive' Main.hs のようにするとよいだろう。

実験用プロジェクトを cabal init で作る場合は cabal new-configure でプロジェクトのビルドで使うghcを変更できる。 このサブコマンドによる変更は cabal.project.local というローカル環境用ファイルに保存される。 プロジェクトを GitHub などに上げるなら .gitignore に追加しておくとリポジトリから環境依存のファイルをなくせる。

cabal new-configure --with-compiler=/path-to-ghc/ghc/inplace/bin/ghc-stage2

それではコードをコンパイラにかけてみよう。

{-# LANGUAGE LinearTypes #-} module Main where flugal :: a ->. (a, a) flugal a = (a, a) wasteful :: a ->. b ->. a wasteful a b = a main = putStrLn "Hello, LinearTypes"

flugal と wasteful はコンパイルエラーになる。 ->. が LinearTypes で有効になる型レベル演算子で、 -> とほぼ同じだが左の値を必ず一度だけ使わなければならないという制限がつく。 flugal は a を2回使っているのでエラー、 wasteful は b を1回も使っていないのでエラーとなる。

より実用的な例も見てみよう。 linear-base パッケージは線形型によるファイル操作API(とそれに必要な諸々)を提供する。 これは現時点で Hackage にアップロードされていないので GitHub から入手する必要がある。 実験用プロジェクトをcabalプロジェクトで作っている場合は、以下の内容で cabal.project というファイルを作ると cabal new-build で GitHub からパッケージを持って来てくれる。

source-repository-package type: git location: https://github.com/tweag/linear-base tag: 0d6165fbd8ad84dd1574a36071f00a6137351637 packages: ./

ファイルを2つオープンし、片方から1行読んでもう片方に書き込むプログラムは以下のようになる。 cabalプロジェクトの場合は cabal new-run で実行できる。 これはちょっと込み入っている。

{-# LANGUAGE LinearTypes #-} {-# LANGUAGE RebindableSyntax #-} {-# LANGUAGE RecordWildCards #-} module Main where import qualified Control.Monad.Linear.Builder as Linear import qualified System.IO.Resource as RIO import Prelude.Linear (Unrestricted(Unrestricted)) import System.IO (IOMode(ReadMode, WriteMode)) import qualified System.IO as System mainRIO :: RIO.RIO (Unrestricted ()) mainRIO = do inHandle <- RIO.openFile "Main.hs" ReadMode outHandle <- RIO.openFile "dup.txt" WriteMode (inHandle', outHandle') <- dupOneLine inHandle outHandle RIO.hClose inHandle' RIO.hClose outHandle' return (Unrestricted ()) where Linear.Builder{ .. } = Linear.monadBuilder dupOneLine :: RIO.Handle ->. RIO.Handle ->. RIO.RIO (RIO.Handle, RIO.Handle) dupOneLine inHandle outHandle = do (Unrestricted l, inHandle') <- RIO.hGetLine inHandle outHandle' <- RIO.hPutStrLn outHandle l return (inHandle', outHandle') main :: System.IO () main = RIO.run mainRIO

いくつかの要素が登場している。 要素ごとに見ていこう。

RebindableSyntax と RecordWildCards は do 記法の実装を通常の Control.Monad から別のものに変更するためにつけている。 なんでそんなことをするのかというと Control.Monad は線形型向けに作られたものではないため評価に線形型の制限が入っていない。 線形型の制限が入ったバージョンの Monad として linear-base は Control.Monad.Linear を提供しており、 線形型 do を使いたい関数の where 節で Linear.Builder{..} = Linear.monadBuilder のようにすると、 RebindableSyntax と RecordWildCards の働きにより do の実装を線形型版 Monad に変更することができる。

RIO は Resource-aware IO の意味で、線形型 Monad 版の IO となっている (rioとは関係ない)。 IOという名前だが現状提供されているのはいくつかのファイル操作だけである。 基本的なAPIと型シグネチャは通常のIOのものと似ているが、 hClose 以外の関数はすべてハンドルも返すようになっているのがポイント。

RIO は RIO.run 関数によって通常のIOの中で起動することができる。 ところどころに出てくる Unrestricted というのは線形型の制限の中から制限のない値を取り出すときに使うデータ型である。

さて、このプログラムで線形型の制限がちゃんと働いているか確かめるには hClose を削ったり2回呼んでみるとか、 dupOneLine で hGetLine や hPutStrLn が返したハンドルではなく引数のハンドルをそのまま返すなどしてみるとよい。 コンパイルエラーのメッセージがどのようなものになるかぜひ確認してみてほしい。

所感

線形型の導入によって一般のHaskellプログラマーは影響を受けるだろうか？

私は影響は限定的だと思う。 線形型は LinearTypes をONにして型シグネチャに ->. を使ってはじめて有効になる。 線形型の導入による実行時システムへの変更もない。 今後リソース系ライブラリで線形型APIを提供するものが現れて、 それを使うのであれば線形型Haskellの書き方を学習する必要はあるだろう。

別に今までのHaskellのリソース管理が危険だったということもなく、 以下の記事に紹介されているようにHaskellにはすでに様々なリソース管理のツールがある。

qiita.com

あえて線形型を必要とするのは、 上記のように低レイヤーのAPIを提供するリソース系ライブラリか、 レイテンシが特に気になるサーバやゲームの開発くらいだろうか。

ひとつ気になることがあるとすれば、線形型GHCを主導しているのが Tweag I/O だということだ。 Tweag はGHCを拡張して asterius という Haskell to WebAssembly コンパイラを開発している。 GHCへの線形型の導入が、将来の Haskell によるWebプログラミングを見据えてのものだという可能性はあると思う。 クライアントサイドWeb開発はまさにレイテンシが重要な領域だからだ。