PureScriptのイータ変換を手助けするライブラリを作りました

2023/12/08に公開

絶対にイータ変換したいでござる!!!
絶対にイータ変換したいでござる!!!

そんな御仁(というか私)のためにイータ変換を手助けするライブラリ作りました。

https://github.com/pujoheadsoft/purescript-eta-conversion

動機

例えば次のようなコードがあったとしましょう。

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 -> cc -> 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