GHCの線形型プロトタイプを試すだけ
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ブランチをビルド
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) -- Error! wasteful :: a ->. b ->. a wasteful a b = a -- Error! 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 -- linear-base import qualified Control.Monad.Linear.Builder as Linear import qualified System.IO.Resource as RIO import Prelude.Linear (Unrestricted(Unrestricted)) -- base 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 -- for do-notation 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にはすでに様々なリソース管理のツールがある。
あえて線形型を必要とするのは、 上記のように低レイヤーのAPIを提供するリソース系ライブラリか、 レイテンシが特に気になるサーバやゲームの開発くらいだろうか。
ひとつ気になることがあるとすれば、線形型GHCを主導しているのが Tweag I/O だということだ。 Tweag はGHCを拡張して asterius という Haskell to WebAssembly コンパイラを開発している。 GHCへの線形型の導入が、将来の Haskell によるWebプログラミングを見据えてのものだという可能性はあると思う。 クライアントサイドWeb開発はまさにレイテンシが重要な領域だからだ。