PureScriptのイータ変換を手助けするライブラリを作りました
絶対にイータ変換したいでござる!!!
絶対にイータ変換したいでござる!!!
そんな御仁(というか私)のためにイータ変換を手助けするライブラリ作りました。
動機
例えば次のようなコードがあったとしましょう。
newtype Result a = Result a
fun :: String -> Int -> Boolean -> String
fun a b c = a <> show b <> show c
そしてこれらを使う関数を定義したいとします。
example :: String -> Int -> Boolean -> Result String
example s i b = Result $ fun s i b
この関数は左辺と右辺の引数の並び順が一致しているので、気持ち的に次のようにイータ変換したいです。
example :: String -> Int -> Boolean -> Result String
example = Result $ fun
が、当然これはできません、なぜならイータ変換するには関数がこうなっていないといけないからです。
example :: String -> Int -> Boolean -> Result String
example = \s i b -> Result $ fun s i b
Result <<< fun
とするのも勿論うまくいきません。
中置演算子<<<
の(関数合成の場合の)定義は次のようになっており、型がマッチしないからです。
compose :: (b -> c) -> (a -> b) -> (a -> c)
次のように引数を1つだけとる関数ならうまくいきます。
exampleA1 :: String -> Result String
exampleA1 = Result <<< identity
では\s i b -> Result $ fun s i b
のように定義したいかというと、これは結局引数を省略できていないし却って冗長になっているので嫌です(私は)。
なので、\s i b -> Result $ fun s i b
のような関数を作る関数を作ることにしました。
使い方
イータ変換できる関数を作る
使い方は簡単で、次のように中置演算子<<|
を使うだけです。
import Data.EtaConversionTransformer ((<<|))
example :: String -> Int -> Boolean -> Result String
example = Result <<| fun
説明
<<|
は関数を2つとり、\s i b -> Result $ fun s i b
のような構造の関数を返します。
この<<|
の定義は次のようになっています。
(o -> ret) -> (引数 -> o) -> (引数 -> ret)
上記の「引数」の部分は可変個の引数を表しており、最大9個までの引数に対応しています。
引数の定義順を左回転させたような新たな関数を生成する
rotate
関数を使うと、次のように引数の定義をずらした新たな関数を生成することができます。
a -> b -> c
をc -> a -> b
に変える感じです。
import Data.ArgsRotater (rotate)
fun :: String -> Int -> Boolean -> String
fun _ _ _ = ""
example :: Boolean -> String -> Int -> String
example = rotate fun
繰り返しrotate
関数を適用することで次々に定義順を入れ替えられます。
rotate2 :: Int -> Boolean -> String -> String
rotate2 = rotate $ rotate fun
rotate3 :: String -> Int -> Boolean -> String
rotate3 = rotate $ rotate $ rotate fun
また、中置演算子<^
を使うと、最後の引数が適用済みの新たな関数を生成することができます。
import Data.ArgsRotater ((<^))
fun :: String -> Int -> Boolean -> String
fun _ _ _ = ""
example :: String -> Int -> String
example = fun <^ true -- 最後の引数(Boolean)は適用済み
<<^
を使うことで、繰り返し最後の引数を適用させられます。
example :: String -> String
example = fun <<^ true <<^ 10
何らかの入力をもとにイータ変換できる関数を作る
次のコードを見てください。
関数をレコード型として持つ型Functions
と、そのレコード型の値を取り出すだけのrunFunctions
という関数があります。
そしてこれらを利用するexample
という関数があります。
newtype Functions = Functions {
fun :: String -> Int -> Boolean -> String
}
runFunctions :: Functions -> { fun :: String -> Int -> Boolean -> String }
runFunctions (Functions r) = r
example :: String -> Int -> Boolean -> Result (Functions -> String)
example s i b = Result $ (\f -> f.fun s i b) <<< runFunctions
この関数example
は中置演算子<<:
を使うことで次のようにイータ変換ができます。
import Data.EtaConversionTransformer ((<<:))
example :: String -> Int -> Boolean -> Result (Functions -> String)
example = Result <<: _.fun <<< runFunctions
イータ変換でReaderTを作る関数を作る
関数を持つレコード型と、そのレコード型から関数を取り出して扱うReaderT
型の値を返すexample
という関数があるとします。
(ReaderT Functions m Unit
とは書けないのでTypeEquals
を使っています)
type Functions m = { fun :: String -> m Unit }
example :: forall r m. TypeEquals r (Functions m) => String -> ReaderT r m Unit
example s = ReaderT $ \r -> (to r).fun s
上記と同様の関数をreaderT
関数を使うことで実現できます。
import Data.ReaderTEtaConversionTransformer (readerT)
example :: forall r m. TypeEquals r (Functions m) => String -> ReaderT r m Unit
example = readerT _.fun
おわりに
これでPureScriptのライブラリを作ったのは2つ目になりますが、演算子をどうするかは常に非常に悩ましい問題ですね。
<<*
,<<#
,<<$
だとメジャーな演算子<*>
や<#>
や<$>
と何か関係がありそうで誤解を招きそうだし、<<=
だと=<<
と関係がありそうだし。
本当は「新たに作った関数の中で渡した関数を実行する」という意味合いを込めて<</
としたかった(/
の部分が中っぽいと思った)のですが、flipされた関数を考えたとき\>>
の\
の部分は下手すると¥マークになって対称性が無くなるな、とか。
結局|
が中ってことを表してるっぽいかも、:
はinputのiっぽいかもと思っていまの形になったわけですが、正解は「無い」んじゃないかなぁという気がします。
<$>
とか<#>
とかやたら沢山ある演算子は見慣れてるだけで、これらの演算子だって初見じゃ意味わからんかもな、と。<<<
や>>>
とかは視覚的にもわかりやすいですけどね。
イータ変換だって最初はわかりづらいなぁと思っていました。
「省略されると情報が失われるじゃん、読みづらくなるからイータ変換できても自分は全部書こうっと。」
と思ってました。
けど場合によってはイータ変換を使った方がシンプルでよくなると思ってきちゃったんですよね。
慣れって怖い(?)ですね。
ということで、皆様よりよいイータ変換ライフを!
Discussion