Shiika/effect
案(2024-10)
code:sk
# エフェクトはEffectクラス(組み込み)のサブクラスとして定義する
class Logger : Effect
def debug(msg: String); end
end
# ハンドラはそのサブクラスとして定義する
class NullLogger : Logger
def debug(msg: String)
end
end
class Main
# エフェクトを使用するメソッドの例
# $が付いた引数は環境から対応するハンドラオブジェクトを取ってくる
# ハンドラオブジェクトが存在することは静的に検査できる
def foo($logger: Logger) -> Result<Void>
$logger.debug("Start foo")
Ok.new(Void)
end
def main
let logger = NullLogger.new
# ハンドラの指定例
logger.handle do
foo()
end
end
end
グローバル変数というものを無くせば記号$を再利用できる。という
with loggerみたいな構文がないとハンドラが重なったときにネストが深くなって見づらくなりそう
気付いたこと
こういうの「グローバル変数にする」(singleton含む)か「いちいち持ち回る」のどちらかだと思ってたけど、第三の選択肢(「環境からマジカルに取得できる」)という感じでとてもおもしろい
かつ、存在が静的に保証されているというのがとても良い
グローバルに持つ場合、初期化順序とかでまだ存在しないロガーにアクセスするパターンがある
Haskell等の場合は「effectfulな計算」が結果になるためシグネチャの返り値のところにエフェクトが現れるが、そういった遅延を行わない場合、返り値は単純な型になる
オリジナルと違いすぎるので、なんか致命的な破綻がありそうな不安はある
検討事項
今まで一級関数の型はFn2<Int, Bool, Bool>みたいに書くつもりでいたけど、シグネチャがエフェクトを含むことになると、この記法は使えなくなる
記法というか、「関数」専用の型を別に分ける必要がありそう
その2
code:sk 他のエフェクトに依存するエフェクトを考える
class Config : Effect
def get(key: String) -> Maybe<String>
end
class Logger : Effect
def initialize($conf: Config)
def debug(msg: String)
end
class FileConfig
... (.tomlなどを読む)
end
class FileLogger : EffectHandler, Logger
def initialize($conf: Config)
let path = $conf.get("log_path").unwrap
let @file = File.open(path)
end
def debug(msg: String)
end
end
class Main
def foo($logger: Logger) -> Result<Void>
$logger.debug("Start foo")
Ok.new(Void)
end
def main
let conf = FileConfig.new("config.toml")
conf.handle do
let logger = FileLogger.new
logger.handle do
foo()
end
end
end
end
その3
「現在時刻の取得」をテスト時だけ固定日時にしたい、みたいのはありがち
code:sk
class Clock : Effect
def now -> Time
Time.now
end
end
class FreezedClock : Clock
def now -> Time
Time.at(2024, 10, 1, 0, 0, 0)
end
end
まあこれだけだと勝手にTime.nowを呼ばれてしまうのは防げないが…
組み込みクラスはすべてエフェクトになっているべきか?(IO, Netなど)
Time.nowがない(Time.new.nowみたいにしないといけない)ということにすると多少間違いが減るかも
あそうか、以下のようにすればいいのかも
Time::Clockという組み込みのエフェクトがある
Time.nowはそれを使う (def self.now($clock: Time::Clock))
デフォルトではClockはトップレベルで自動的にハンドルされるが、オプションでオフにすることもでき( #Koka のmask的な)、そうすると意図しないTime.nowはコンパイルエラーになる その4
こういうログを出すやつ
code:txt
Start
Task1 start
Task1 doing
Task1 end
End
code:sk
class Logger : Effect
def msg(s: String)
puts s
end
end
class SubLogger : Logger
def initialize(@name: String); end
def msg(s: String, $logger: Logger)
end
end
class App
def main
Logger.new.handle do
SubLogger.new("Task1").handle do
task1
end
end
end
def task1($logger: Logger)
$logger.msg("start")
$logger.msg("doing")
$logger.msg("end")
end
end
その5 Rustの?
code:sk
base class OnError : Effect
def error(msg: String, k: Continuation); end
end
class ShowError : OnError
def error(msg: String, k: Continuation)
p msg
end
end
class LogError : OnError
def error(msg: String, k: Continuation)
@file.puts(msg)
end
end
class App
def main
ShowError.handle do ... end
LogError.new("log.txt").handle do ... end
end
def read_config(path: String) -> Result<Json>
# エラーになったらearly returnする
let Ok(txt) = File.read(path) else return Error.new("failed to read #{path}") # 前考えてた記法
let Ok(txt) = File.read(path).try!
# これらはエラーというeffectをperformすると考えられないか
let Ok(txt) = File.read(path) else $onError.error("failed to read #{path}") end
end
違うか。そもそもResult型を使わないということになるのかな。
$on_error: OnErrorがthrowsのようなマーカーになる
$on_error.error(...)がthrowに対応する
XxError.handleがtry-catchに対応する
書き味としては例外と変わらない?
Resultのよさ(エラーとなり得る箇所が必ず可視化される)は失われる