Cleanという言語で副作用を扱うのに使われている一意型について、簡単に調べてみました。モナドが使われるようになった背景を理解する一助になるかもしれません。
※ この記事はHaskellの知識を前提としています。Haskellから推測できる事項については説明を省略しています。
この記事には続編のようなものがあります。
- IOモナドを素手で触ってみた 2014.12.9
Clean
CleanはHaskellとよく似た非正格純粋関数型言語です。
CleanとHaskellは方言程度の違いです。次の記事に比較がまとめられています。
なぜ似たような言語が2つあるかというと、副作用の扱い方に違いがあるためです。Haskellではモナドを使うのに対して、Cleanでは一意型(uniqueness type)を使います。
Concurrent Cleanという名前で呼ばれることもありますが、これは並列性にシフトしようとした時期の名残らしいです。
ドキュメント
- 原文: Functional Programming in Clean - Clean
- 日本語訳(Internet Archiveより): Cleanで関数プログラミング 【総目次】 2002.3.20
一意型
一意型についてドキュメントで調べたことをまとめます。単純な引用ではなく必要に応じて解釈を加えているため、表現が不正確である可能性があります。
4.3.4 一意性型付けより、一意型とは次のような意味を持つようです。
- 一意型とは:一意性を持つ型
- 一意性とは:一度しか使われないこと
次のプログラムを例にして、このコードに至る過程を説明します。
module test
import StdEnv, StdFile
WriteAB :: *File -> *File
WriteAB f
# f = fwritec 'a' f
f = fwritec 'b' f
= f
Start :: *World -> *World
Start w
# (f, w) = stdio w
f = WriteAB f
(_, w) = fclose f w
= w
ab65536
- Haskellと異なり、関数を小文字で始める制約はありません。
-
stdio
で標準入出力のハンドルを取得して、それに対してWriteAB
で'a'
と'b'
を書き込んでいます。 -
Start
の戻り値を表示する仕様のため、実行結果にはw
のハンドル番号65536
が含まれます。(消す方法は不明です)
このコードはドキュメントに断片的に記載されているサンプルを補完したものです。次の記事を参考にしました。
- @hiratara: Hello clean world! - Qiita 2012.7.14
一意性
C言語のFILE*
のように、他の言語であれば取得したファイルハンドルを使い回して書き込むのが普通です。
同様の発想で'a'
と'b'
を書き込もうとしても、Cleanではエラーになります。
4.3.4 一意性型付けより一部改変
WriteAB :: *File -> (*File, *File)
WriteAB file = (fileA, fileB)
where
fileA = fwritec 'a' file
fileB = fwritec 'b' file
Uniqueness error [test.icl,5,WriteAB]: "file" demanded attribute cannot be offered by shared object
Uniqueness error [test.icl,5,WriteAB]: "file" demanded attribute cannot be offered by shared object
型注釈の*
は一意性の指定です。file
には型注釈で一意性が指定されていますが、一意性とは一度しか使われないことのため、2か所で使われているとエラーになります。
修正すると次のようになります。
WriteAB :: *File -> *File
WriteAB file = fileAB
where
fileA = fwritec 'a' file
fileAB = fwritec 'b' fileA
let-before式という書き方もあります。
WriteAB :: *File -> *File
WriteAB file
# fileA = fwritec 'a' file
# fileAB = fwritec 'b' fileA
= fileAB
受け渡しをフローで考えると良さそうです。
- 引数 →
file
→fileA
→fileAB
→ 戻り値
このように一意性変数を次々と受け渡していくスタイルです。一度受け渡してしまったら、受け渡し元には触ることができなくなります。これを検出するのに、複数個所から参照されていないかを調べるようです。
過去の時点のインスタンスに触れないようにすることで、破壊的変更が行われていることを隠蔽して、参照透過性が確保されるという理屈のようです。
類似性
破壊的変更を隠蔽するという点は、STモナドに似ていると思いました。
受け渡された一意型変数が無効になるという点は、C++のムーブセマンティクスに似ていると思いました。一意型変数の受け渡しは常にムーブとして扱われるという解釈です。
参考
STモナドについては次の記事に詳しいです。
次の記事にはIOモナドの実体がRealWorldを取り回すSTモナドであると書かれており、RealWorldは一意型であると解釈できそうです。
- @uehaj: HaskellにおけるIOモナドとSTモナドの関係 - uehaj's blog 2014.1.29
Cleanでは表に出ていて、Start
には*World
が渡されます。
- @hiratara: cleanは副作用を扱うのにIOモナドを使っていない - Qiita 2012.7.14
C++のムーブセマンティクスについては次の記事に詳しいです。
- @yohhoy: 本当は怖くないムーブセマンティクス - yohhoyの日記(別館) 2012.12.15
シャドウイング
受け渡すたびに別名を付けるのは面倒ですが、#
ではシャドウイング(同じ名前の別の変数を定義すること)ができます。
※ ドキュメントではシャドウイングという言い方はされていませんが、意味からそのように解釈しました。
WriteAB :: *File -> *File
WriteAB file
# file = fwritec 'a' file
# file = fwritec 'b' file
= file
2つ目以降の#
は省略できます。
WriteAB :: *File -> *File
WriteAB file
# file = fwritec 'a' file
file = fwritec 'b' file
= file
これはHaskellでレイアウトにより連続するlet
をまとめられるのと同じ発想のようです。let-before式という呼び方から、#
がlet
と類似であることも示唆されています。
main = do
let a = 1
b = 1
print $ a + b
ここまでの知識で書いたのが、先に掲載したコードです。
他のスタイル
5.2 環境渡し技術には他のスタイルも紹介されています。
戻り値を次の関数に渡す
WriteAB :: *File -> *File
WriteAB file = fwritec 'b' (fwritec 'a' file)
多数が連続するとこの方法では大変です。ドキュメントには、記述された順番が実行順と逆転していることも指摘されています。
関数合成
Cleanでは関数合成はo
で表記します。
WriteAB :: *File -> *File
WriteAB file = (fwritec 'b' o fwritec 'a') file
この方法でも、記述された順番が実行順と逆転しています。
Cleanの関数はカリー化されているため、部分適用を利用すれば引数を省略できます(ポイントフリースタイル)。ただしHaskellとは異なり、引数がない関数の型注釈には括弧が必要です。
WriteAB :: (*File -> *File)
WriteAB = fwritec 'b' o fwritec 'a'
型注釈に括弧がないとエラーになります。
WriteAB :: *File -> *File
WriteAB = fwritec 'b' o fwritec 'a'
Error: This alternative has 0 arguments instead of 1.
どうやら表記上の引数を数えているようです。とりあえずポイントフリースタイルでは型を括弧で囲むと認識しておけば良さそうです。
seq
リストに連続適用する関数seq
を使う方法です。Haskellのsequenceに相当するようです(モナド関連の差異はありますが)。見た目がF#のシーケンスに似ていますが別物で、Cleanの方はただの関数と引数です。
WriteAB :: *File -> *File
WriteAB file = seq [fwritec 'a', fwritec 'b'] file
引数を省略します。
WriteAB :: (*File -> *File)
WriteAB = seq [fwritec 'a', fwritec 'b']
改行して変形すると、do記法まであと一歩という印象です。
WriteAB :: (*File -> *File)
WriteAB = seq
[ fwritec 'a'
, fwritec 'b'
]
fwritec
の繰り返しをmap
で書き換えます。ドキュメントでは関数合成を使っていますが、Clean 2.4ではエラーになります。文法が変わったのかもしれません。
WriteAB :: (*File -> *File)
WriteAB = seq (map fwritec ['a', 'b'])
Haskellでは文字列は文字のリストのため['a', 'b']
は"ab"
と等しいですが、Cleanでは区別されるようです。
文字のリストには糖衣構文があります。
WriteAB :: (*File -> *File)
WriteAB = seq (map fwritec ['ab'])
foldl
seq
とmap
を併用するよりもfoldl
を使った方が良いような気がしたので試してみました。flip
が必要になってしまいます。
WriteAB :: *File -> *File
WriteAB file = foldl (flip fwritec) file ['ab']
もう1回flip
すれば引数を消せます。
WriteAB :: (*File -> *File)
WriteAB = flip (foldl (flip fwritec)) ['ab']
ここまで来るともうパズルです。seq
とmap
の方がシンプルでした。
<<<
ファイルに書き込むための専用演算子です。C++のiostreamに似ています。
WriteAB :: *File -> *File
WriteAB file = file <<< 'a' <<< 'b'
これはあくまで個別対応なので、ファイル以外で同じようなことがやりたければ、それ用の演算子を定義することになるようです。
モナド
色々な書き換えを見て来ましたが、どれも涙ぐましい努力です。ケースバイケースで最適な書き方を見付けるパズルのようですが、統一的に扱えないものでしょうか。
というわけで行き着くのがモナドです・・・ほとんど誘導尋問ですが。
CleanにはUnit型がないので自前で定義します。行頭の::
はデータ型の宣言で、:==
で型シノニムを定義します(コメントで対応するHaskellを示します)。値は0
で代用します。
:: Unit :== Int // [Haskell] type Unit = Int
unit x = (0, x)
5.2.2 モナドスタイルを参考にモナド化してみます。モナド関連の型や関数は標準ライブラリ(StdEnv)に入っています。
fwritecM ch = unit o fwritec ch
WriteAB :: (*File -> (Unit, *File))
WriteAB =
fwritecM 'a' `bind` \_ ->
fwritecM 'b'
Start :: *World -> *World
Start w
# (f1, w1) = stdio w
(_ , f2) = WriteAB f1
(_ , w2) = fclose f2 w1
= w2
do記法に相当するものはないようです。もしあれば、もうモナドでいいじゃんとなって、それがHaskellという感じになりそうです。
実際にHaskellでIOモナドを剥がしてみると、下からは一意型とほとんど同じ世界が現れます。
{-# LANGUAGE UnboxedTuples #-}
import GHC.Base
main = IO $ \world ->
let (# world1, _ #) = unIO (print "hello") world
(# world2, _ #) = unIO (print "world") world1
in (# world2, () #)
"hello"
"world"
Haskellでは世界の受け渡しはモナドによって隠されているため直接触ることはなく、一意性はモナドによって守られているため、言語面での一意型のサポートはありません。
参考
Unit型がないのは、次の記事を参考にしました。
do記法を疑似的に再現しようと試行錯誤も行われたようですが、なかなか厳しいようです。
感想
あくまで個人的な感想ですが、現時点での言語の知名度を見る限り、Haskell(モナド)が勝ってClean(一意型)が負けたように見えます。
難解と評判のモナドに対して、一意型は取っ付きやすさが売りのように感じます。まだHaskellが今のように関数型言語の代表として認知される前は、Cleanに軍配を上げる方もいました。(一意型が理由ではありませんが)
当時の私はCleanどころか関数型言語も知らない状態で、興味を持つのが完全に周回遅れになってしまいました。
一意型
実際に一意型を見ると、受け渡しが面倒でその対策としてモナドがあるということから、一意型は過渡期の技術だという印象を受けました(モナド前史というか)。ただ、発想そのものは確かにシンプルで、モナドほどの取っ付きにくさがないのも事実です。
Haskellを説明する資料にも、比較対象として一意型に言及したものがあります。
- @tanakh: 関数プログラミング入門 - SlideShare 2010.3.28
この資料にはHaskellではモナドの前に遅延ストリームというものを使っていたとあります。遅延ストリームについては次の記事が詳しいです。
- Haskellの入出力 2010.9.16
一意型とモナドを段階的に使い分けるというのも1つの案ではあります。しかしHaskellは既にモナドをベースにPreludeが構築されているため、一意型が入り込む余地はありません。逆にCleanでモナドを使おうとすると、do記法のサポートがないことがネックとなります。
Scala
一意型に復活の余地があるのかはよく分かりませんが、Scalaで何か動きがあったようなツイートを見付けました。
Scala に舞台を移して行われる線形型(一意型)とモナドの戦い!! http://twitter.com/kmizu/status/12793353913503744 http://twitter.com/ittayd/statuses/12731908429451264
— shelarcy(しぇらーしぃ) (@shelarcy) 2010, 12月 9
関連すると思われる資料です。
Scalaの現状を知らないため、一意型がどの程度活用されているのかは不明です。
線形型
先ほどのツイートには線形型という用語が出ていますが、微妙に違うものらしいです。
@eagletmt @tanakh http://bit.ly/aj6Zsd "線形型=もう今後コピーしない ・ 一意型=今までコピーされてない" という説明を聞きます。使い方としてはどちらも結局一意性の保証に使われていることが多いので、まあどっちでも良いような感じもしますが…
— kinaba (@kinaba) 2010, 3月 29
次の記事には、線形論理との関係が示唆されています。
- @sshi: Concurrent Clean - sshi.Continual 2006.2.20
線形型を推しているのがATS言語です。
一意型の敵を線形型で討つ、となるのでしょうか。。。
勉強会
Cleanの勉強会があったようです。
- スタートClean - PARTAKE 2011.7.9
- @oskimura: Clean 概説 - SlideShare 2011.7.10
- Start::Clean - Togetterまとめ 2011.7.9
- @eldesh: この夏、Cleanを始めよう! - ::Eldesh a b = LEFT a | RIGHT b 2011.7.7
- @eldesh: スタートClean、終わりました - ::Eldesh a b = LEFT a | RIGHT b 2011.7.11