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等でインストールできるはず。

ghcUnix系環境であれば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) -- Error!

wasteful :: a ->. b ->. a
wasteful a b = a -- Error!

main = putStrLn "Hello, LinearTypes"

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

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

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

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

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

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

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

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

所感

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

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

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

qiita.com

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

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