在学习OCaml半年之后,我仍然在functional programming
和imperative programming
位苦苦挣扎。
不关于using list or array
,但关于API设计。
例如,如果我要为用户撰写stack
,我应该以{{1}}还是functional
方式展示?
imperative
应该有一个名为stack
的函数,这意味着将最后一个元素返回给用户并将其从堆栈中删除。因此,如果我以pop
的方式设计我的stack
,那么对于functional
,我应该返回一个元组pop
,对吗?但我认为这很难看。
与此同时,我觉得(last_element, new_stack)
方式在功能编程中更自然。
那么,我该如何处理这种设计问题呢?
修改
我看到了functional
的源代码,他们定义了这样的类型:
stack
好的,内部标准的lib使用type 'a t = { mutable c : 'a list }
这是不可变的,但是将它封装在一个可变的记录中。
我理解这就是这样,对于用户来说,它总是一个堆栈,所以不需要元组返回客户端。
但是,它仍然不是一种功能性的方式,对吗?
答案 0 :(得分:4)
可变结构有时更有效,但它们不是持久性的,这在各种情况下都很有用(主要用于回溯失败的计算)。如果不可变接口在可变接口上没有或几乎没有性能开销,那么你绝对应该选择不可变接口。
答案 1 :(得分:1)
在功能上(即没有可变性),您可以使用head / tail而不是pop来定义它与List
完全相同,或者您可以按照建议让API通过返回元组来处理状态更改。这与州monad的建立方式相当。
因此,父作用域的责任是处理堆栈的状态(例如通过递归),在这种情况下,堆栈与列表完全相同,或者某些责任通过元组加载到API。
这是一个快速尝试(假装知道O'Caml语法):
module Stack =
struct
type 'a stack = 'a list
let empty _ = ((), [])
let push x stack = ((), x::stack)
let pop (x::stack) = (x, stack)
| pop _ = raise EmptyStack
end
一个用例就是:
let (_, st) = Stack.empty ()
let (_, st) = Stack.push 1 Stack.empty
let (_, st) = Stack.push 2 st
let (_, st) = Stack.push 3 st
let (x, st) = Stack.pop st
您可能希望隐藏st
的传递,而不是明确地处理元组,而是发明一个运算符,使得以下语法成为可能:
let (x, st) = (Stack.empty >>= Stack.push 1 >>=
Stack.push 2 >>= Stack.push 3 >>= Stack.pop) []
如果你可以制作这个操作符,你就重新发明了状态monad。 :)
(因为上面的所有函数都将状态作为其最后一个参数,它们可以部分应用。为了扩展它,所以更明显的是发生了什么,但是可读性较差,请参阅下面的重写。)
let (x, st) = (fun st -> Stack.empty st >>= fun st -> Stack.push 1 st
>>= fun st -> Stack.push 2 st
>>= fun st -> Stack.push 3 st
>>= fun st -> Stack.pop) []
这是处理状态和不可变数据结构的一种惯用方法,至少。