我正在尝试学习如何使用bind
或map
等函数来撰写 monads,到目前为止,我的代码可以工作,但它真的很冗长。这是我刚刚写的一个小脚本来说明我的意思,它将一个string
投射到一个int option
中,如果有的话,数字会加倍。
open Base
open Stdio
let is_number str = String.for_all ~f:(Char.is_digit) str
let str_to_num_opt (str: string) : int option =
if is_number str then Some (Int.of_string str)
else None
let double n = n * 2
let () =
let maybe_num = Option.map (str_to_num_opt "e123") ~f:double in
match maybe_num with
| Some n -> printf "Number found in string : %dn" n
| None -> printf "No number found in string.n"
maybe_num
的定义相当冗长,组合链越长,情况就会呈指数级恶化,所以我尝试使用>>|
运算符,但由于它附加到Option
模块,我不能直接将其用作中缀函数,而是将其称为Option.(>>|)
(这与使用Option.map
基本相同,但我丢失了命名参数)。
我所做的是在文件顶部添加open Option
,然后重写我的()
函数,如下所示:
let () =
let maybe_num =
str_to_num_opt "123"
>>| double
in match maybe_num with
| Some n -> printf "Number found in string : %dn" n
| None -> printf "No number found in string.n"
代码现在干净多了,但我只能对每个文件的一个模块使用这个技巧,因为如果我在文件顶部的open Option
之后添加open List
(例如),它将遮蔽>>|
的定义,运算符将只处理列表,从而破坏我的代码。
我所希望的是同时共存>>|
的两个定义,并让编译器/解释器在运行代码时选择具有正确签名的那个(类似于在 Rust 中为不同类型的trait
实现),但我无法让它工作。甚至可能吗?
在 OCaml 中,要执行您想要的操作,您可以在let
块内以let open List in ...
形式本地打开模块(有关其他形式的本地打开,请参阅 https://v2.ocaml.org/manual/moduleexamples.html#s%3Amodule%3Astructures 的手册),并且可以隐藏变量,例如,您可以通过引用List.map
为某些代码定义运算符>>|
,随后通过引用Option.map
为同一文件中的其他代码重新定义它。
同样,您可以有一个定义let ( let* ) = Option.bind
,然后有一个let ( let* ) = Result.bind
其他代码的影子定义。
但是,OCaml 不实现临时多态性(也称为函数重载),因为编译器不会自动为您选择正确版本的运算符>>|
。 将来可能会也可能不会通过模块化隐式提供此功能。
另一种解决方案是为经常使用的每个模块定义绑定。因此,例如,您可以使用let*
表示Option.bind
,let+
用于Result.bind
,...这样做的优点是使用起来非常短且轻量级,同时又是明确的(因为您可以通过最后一个字符识别 monad)。您甚至可以将所有此类定义放入它们自己的模块中,您将在每个文件的开头open
这些模块,这样您就不必每次都重新定义绑定。