Functional programming 在OCaml中是否有一种惯用的方法来实现隐式局部状态?

Functional programming 在OCaml中是否有一种惯用的方法来实现隐式局部状态?,functional-programming,ocaml,state-monad,Functional Programming,Ocaml,State Monad,我想写一些代码,使用一些本地状态构建一个东西。例如,考虑使用本地状态生成序列整数的以下代码: type state=int ref let uniqueId:(state->int)= 乐趣->增加!s 让我们做些事情:((陈述->'a)->'a)= 趣味身体->身体(参考0) 让()= 让x1=makeThings(有趣的s-> 设i=in(*1*)中的唯一ID s 我 )在 打印int x1;打印新行();(*打印1*) (*每个makeThings回调都有自己的本地状态。 ID再次从1开始

我想写一些代码,使用一些本地状态构建一个东西。例如,考虑使用本地状态生成序列整数的以下代码:

type state=int ref
let uniqueId:(state->int)=
乐趣->增加!s
让我们做些事情:((陈述->'a)->'a)=
趣味身体->身体(参考0)
让()=
让x1=makeThings(有趣的s->
设i=in(*1*)中的唯一ID s
我
)在
打印int x1;打印新行();(*打印1*)
(*每个makeThings回调都有自己的本地状态。
ID再次从1开始生成*)
让x2=制造事物(有趣的s->
设i=in(*1*)中的唯一ID s
设j=in(*2*)中的唯一ID s
i+j
)在
打印int x2;打印新行();(*打印3*)
()
我很好奇是否有一种方法可以使makeThings回调中的
s
state参数隐式,这样我就不需要反复键入它,从而保证所有
uniqueId
调用都通过相同的状态参数。例如,在Haskell中,您可以使用monads和do表示法来结束代码

makeThings$do

我单子也在OCaml中工作。由于pa_monad_自定义语法扩展,您甚至可以使用do符号。虽然,在大多数情况下,只有一个中缀绑定操作符,即
>=
就足以编写一个奇特的代码

您的代码看起来像是一元风格+引用的奇怪混合体。。。如果要限制本地状态仅通过特定方式更改,则应将其隐藏在本地上下文中:

let make_unique_id init = 
  let s = ref (init - 1) (* :-) *) in
  fun () -> 
    incr s;
    !s
s
现在隐藏在闭包中。因此,您可以创建相互独立的计数器:

let () =
  let x1 =
    let unique_id = make_unique_id 1 in
    let i = unique_id () in
    i
  in
  print_int x1; print_newline (); (* Prints 1 *)

  let x2 = 
    let unique_id = make_unique_id 1 in 
    let i = unique_id () in     (* 1 *)
    let j = unique_id () in     (* 2 *)
    i + j
  in
  print_int x2; print_newline () (* Prints 3 *)

听起来你想要一个全局变量

let currentId = ref 0

let uniqueId () = 
  incr currentId;
  !currentId
您认为全局变量是不可取的,但您指定的行为(“对uniqueId的所有调用都传递相同的状态参数”)正是全局变量的行为

如果您担心其他代码访问全局变量,请不要在模块的签名(
.mli
文件)中公开
currentId

如果您担心同一模块中的其他代码访问
currentId
,则可以将其范围限制在
uniqueId
的定义中:

let uniqueId =
  let currentId = ref 0 in
    fun () -> 
      incr currentId;
      !currentId
或者创建一个子模块,该子模块不在签名中公开
currentId

module M : sig

  val uniqueId : unit -> int

end = struct

  let currentId = ref 0

  let uniqueId () = 
    incr currentId;
    !currentId

end

include M

就个人而言,我会选择第一种解决方案(全局变量隐藏在
.mli
文件中)。不难确保同一模块内的其他代码不会滥用
currentId
,并且模块系统会保护您免受其他代码的侵害。

您已经拥有的代码略有变化。不使用延续,只需提供一个函数来生成新状态:

module State : sig

  type t

  val fresh : unit -> t

  val uniqueId : t -> int

end = struct

  type t = int ref 

  let fresh () = ref 0

  let uniqueId s = incr s; !s

end

let () =
  let x1 =
    let s = State.fresh () in
    let i = State.uniqueId s in
      i
  in
    print_int x1;
    print_newline () (* Prints 1 *)

let () =
  let x2 =
    let s = State.fresh () in
    let i = State.uniqueId s in     (* 1 *)
    let j = State.uniqueId s in     (* 2 *)
      i + j
  in
    print_int x2;
    print_newline () (* Prints 3 *)

这是一种在编译器中处理环境的常用方法,看起来非常像您正在尝试执行的操作。它不会隐式地遍历状态,因为OCaml不支持隐式参数。但是,如果您只需要一个这样的“环境”参数,那么将其添加到所有适当的函数中并不太麻烦。

我想您希望实现的是:

  • 具有
    状态的函数
    f
    (make_things)
  • 每次调用
    f
    ,状态都会重置
  • 但是在
    f
    的一次调用中,状态可以自动更改
  • 如果我是正确的,那么我们不需要Monald,相反,我们可以使用memo

    let memo_incr_state () =
        let s = ref 0 in
        fun() -> s := !s + 1; !s
    
    let make_things f = 
        let ms = memo_incr_state() in
        f ms
    
    let f1 ms = 
        let i = ms() in
        let j = ms() in
        i+j
    
    let x1 = make_things f1 (* x1 should be 3 *)
    
    基本的想法是我们用thunk来记住状态


    更多关于记忆的知识可以从

    中获得,但人们在实践中是否使用单子来编写这种代码?如果你能想到一个这样做的项目,链接到它会让我的生活更轻松:)通常我们使用monads作为IO。我个人从未在生产代码中见过状态单子。我的个人偏好是显式地传递状态,并在状态发生更改时返回它。如果makeBody的回调调用其他函数,那么它将需要向它们传递
    unique\u id
    ,我们回到了启动时的状态。。。我得到的印象是,这只是将状态封装在一个OO模式中(优点和缺点),但并没有解决我最初遇到的“围绕状态线程化”问题。至于奇怪的引用+一元风格,这就是为什么我要问这个问题:)我认为使用可变状态比通过单子使用假状态更清楚,但我对ocaml是新手,所以我知道什么。如果你想防止一个
    unique\u id
    函数在多个地方被误用,请定义一个更高阶的函数,如
    with\u unique\u id:((单位->整数)->“a)->'a
    其参数采用了由
    make_unique_id
    生成的新函数。顺便说一句,如果你想要状态monad的纯解,你应该保持事物的纯。在一元状态下有一个引用是非常令人困惑的。即使我按照Leo White的回答将状态设为抽象类型,使用引用是否也很糟糕?没关系。我的关键是,如果您想使用状态monad,您也可以在OCaml中使用它,而且您根本不需要使用ref。我同意@ivg在OCaml中使用状态monad的观点:我有时使用它,但很少使用它,以至于我感觉不到do符号。我不确定这是否正是我想要的。每次调用
    makeThings
    都应该从0开始生成IDID不是一个全局递增的数字(这是因为在我的实际用例中,状态是在makeThings调用之间需要清除的事物列表)。此外,如果我使用一个模块封装状态,是否有方法实例化同一模块的两个实例,或者我将被限制为一个完整的实例?您可以使用一个模块使用一个函子或一级模块将状态封装为多个实例。但在这种情况下,它不会给您带来任何好处,您也可以只传递一个实例d
    updateId
    。我添加了