关于 Haskell 当中使用 mutable 数据的一些资料

Haskell 教程的问题

网上扒了不少链接, 看了以后对问题有点改观, 但是消化不掉[br]所以整理一下放在这里, 希望有点提升, 而且可以讨论下这个问题[br]Clojure 教程当中明明白白讲过 Atom, 所以可变数据的态度明确[br]Clojure 里就是整体用不可变数据, 而可变的部分用 Atom 包裹起来[br]到了 ClojureScript 更放开了调用 JavaScript 代码时的可变状态[br]由于 Clojure 没有强制封装副作用, 整体感觉是比较轻松的

与 Clojure 对比, Haskell 里对于可变数据, 在教程上极少提及[br]作为纯函数语言, 而且限制副作用, 因而 IO 相关的篇幅很晚才介绍[br]比如 learn you a haskell 半本过去还是纯数据, 都没有讲副作用[br]我猜测这也许是 Haskell 推广的一个问题, 副作用讲得太不清楚了[br]对于应用开发者而言, 文件, 数据库, 图形界面, 网络, 这才是工作的主体[br]当 Haskell 对我们工作的主体部分做大量的限制, 等于限制我们的工作

比如我在写网页应用时, 大量的 DOM 操作, 网络读取, 都是副作用[br]直到 React 对 DOM 整体进行了封装, 事情才有所改观[br]但即便这样, 副作用还是任务的主体, 在纯的组件之外, 丝毫不减少副作用[br]我回顾自己学 Haskell, 熟悉了高阶函数很多用法, 但类型和副作用仍然菜鸟[br]除了 cljs 更合适的原因之外, 我真的开始认为 Haskell 教程设计存在问题[br]对于副作用的讲解和改进, 做得也许真的不够多

比如 IORef 这个东西, 我刷了几年社区了, 根本不知道有[br]交流会第二天早上我试着去搜 Haskell 有没有办法模拟 Clojure 的 Atom[br]结果给我搜索到了, Wiki 上有, StackOverflow 有, 个人博客有[br]然而却不在教程上, 倒是 PureScript 那份很长的教程结尾有一些[br]所以结论是, Haskell 当中有可变数据, 但很少有介绍, 而且不推荐用[br]从这个角度看 Haskell 的教程真的没有把我们照顾好

可变状态的需求

只是说我遇到的例子吧, 在用了 Clojure 之后发现还是很难干掉 mutable 状态[br]在 MVC 中, 虽然 View 组件内部, Model 单页内部, 可以做到 pure[br]但是在 MVC 各个部分之间, 难以梳理形成直白的依赖关系[br]比如说 Model 的更新, 下一个 Model 依赖上一个 Model 状态, 自己依赖自己[br]Controller 要把 MVC 连接成一个循环, 而循环导致循环引用[br]单纯靠不可变数据的写法, 不可能构造出这样一个程序出来[br]加上 View 比如是 DOM, DOM 是可变状态, 很难说用不可变数据去搞定

在 React 社区当中各种观点说, 需要用 Immutable 来增强性能和可靠[br]但 Immutable 是体现在组件共享数据的过程当中, 并不是所有位置[br]这也许是我学习过程的一个误区, 以为 Immutable 解决一切, 实际当然不是[br]而 js 整体可变带来一种误解, 我们不知道区分数据可变和不可变[br](在 C 层级的语言中有 const, 那属于硬件性能因素, 场景不同不讨论)[br]换成 Atom 的概念就是, 数据不可变的, 但会用到可变的引用

比如有一个状态称为 a, 最开始数据是 a0, 经过操作后数据是 a1[br]那一般我们就理解 a 是从 a0 变成了 a1, 在 js 里写 a=a0 和 a=a1[br]但按照 Atom 的概念仔细想, 不对, a 和 a0 a1 的类型是不一样的[br]比如说 a 是车的位置, 那么 a 将随着时间改变, 相当于 f(t)[br]而 a0 作为车的位置, 它却不变, 为什么, 因为它是具体一个时间的位置[br]一个是跟时间相关的状态, 一个是坐标, 怎么能完全一致呢?[br]在 js 里没有区分, 但是 Clojure 区分了, a 是 Atom, 类型不一样

编程里黑科技太多, 我也不敢把话说绝, 但至少我认为这是一个场景[br]这种情况下不可变数据是可靠的, 而可变状态也是被需要的[br]Haskell 作为通用编程语言, 缺失这种功能太不合常理了[br]实际上可变状态这种东西, Haskell 从来没说过没有, 只是我会错意了[br]Haskell 说, 没有可变数据, 所有状态都是隔离的[br]而且教程上不会教我怎么去写模拟 Atom 的可变状态这种东西 - -!

IORef, State

然后我就搜索到了各样一个问题, 怎么模拟全局变量:[br]Haskell - simulate global variable(function)[br]问题的回答里首先给了经典的 do 表达式封装局部状态的写法[br]然后才开始讲万不得已的话, 用 ugly tricky 的 IORef:

import Data.IORef
import System.IO.Unsafe
import qualified Data.Map as Map

{-# NOINLINE funcs # -}
funcs :: IORef (Map.Map String Double)
funcs = unsafePerformIO $ newIORef Map.empty

f :: String -> Double -> IO ()
f str d = atomicModifyIORef funcs (\m -> (Map.insert str d m, ()))

g :: IO ()
g = do
fs
g 和 f 两个函数分别去读和写, 好吧, 真的很像全局变量了[br]然后第二个答案又来一个局部变量, r, 每次读取时在闭包里 +1:

import Data.IORef

type Counter = Int -> IO Int

makeCounter :: IO Counter
makeCounter = do
r do modifyIORef r (+i)
readIORef r)

testCounter :: Counter -> IO ()
testCounter counter = do
b
马上脑补一个套路一样的 CoffeeScript 版本, 看闭包:

makeCounter = ->
r = 0
(i) ->
r = r + i
r

testCounter = (counter) ->
b = counter 1
c = counter 1
d = counter 1
console.log [b,c,d]

do ->
counter = makeCounter()
testCounter counter
testCounter counter
所以, Haskell 中是有办法模拟出可变状态的, 用的是比如 IORef[br]答案里也说了, 比较乱来的黑科技, 不推荐用(用了就不严谨不安全了)[br]然后除了 IORef, 还有 STRef, 比如这两个提问:[br]When is it OK to use an IORef?[br]When to use STRef or IORef?

扒到一篇博客讲 IORef, ST, MVar 这几个事情的, 跟并行计算有关[br]Mutable State in Haskell[br]开头有句话我觉得比较适合用来解释 Immutable 到底算什么:

All variables are indeed immutable, but there are ways to construct mutable references where we can change what the reference points to.

数据是不可变的, 只是有时候需要一个可变的引用来帮助指向不同的数据[br]文章还给了一点例子, 其中的写法就和 Clojure 里的 Atom 挺像了:

import Data.IORef

main :: IO ()
main = do
ref >= print
Clojure 显得短一些(对比下 print, Clojure 是不考虑副作用的类型问题):

(defn -main []
(let [ref (atom 0)]
(swap! ref inc)
(println @ref)))
整个文章大部分看不懂, 只是了解一下其中可变状态的处理[br]但是可以看到, Haskell 即便可以搞出可变状态, 它还是做了隔离的[br]IORef 返回的结果, 依然包裹在一个 IO 当中, 而不是直接的值[br]通过这一点, Haskell 还是会把副作用给识别出来, 而不像常见编程语言[br]虽然我不喜欢, 但确实要程序更可靠还是少不了借助这类办法

对了, 忘了贴一个官方 Wiki 上的相关文章, 看不懂地方更多 - -![br]Top level mutable state

PureScript 中的副作用

PureScript 听说在状态方面抽象得更细致一点[br]因为要编译到 JavaScript, 需要涉及状态的地方多了许多[br]不过它总体的文档不多, 大概找到几个可变状态的介绍:[br]http://www.purescript.org/learn/eff/

collatz :: Int -> Int
collatz n = pureST do
r
大致上是 Haskell 的 STRef, 我看不出区别,[br]编译结果的 JavaScript 是这样:

var collatz = function (n) {
return Control_Monad_Eff.runPure(function do() {
var r = n;
var count = 0;
(function () {
while (!(function
do() {
count = 1 + count;
var m = r;
r = (m % 2 === 0) ? m / 2 : 3 * m + 1;
return m === 1;
})()) {
};
return {};
})();
return count;
});
};
另外在书上还写了一些, 写得大概能看懂, 但是我估计自己不会用:

https://leanpub.com/purescript/read# leanpub-auto-global-mutable-state[br]https://leanpub.com/purescript/read# leanpub-auto-mutable-state

看下找到的代码的例子:

https://github.com/purescript/purescript/blob/master/examples/passing/Eff.purs

module Main where

import Prelude
import Control.Monad.Eff
import Control.Monad.ST
import Control.Monad.Eff.Console

test1 = do
log "Line 1"
log "Line 2"

test2 = runPure (runST (do
ref n + 1.0
readSTRef ref))

test3 = pureST (do
ref n + 1.0
readSTRef ref)

main = do
test1
Control.Monad.Eff.Console.print test2
Control.Monad.Eff.Console.print test3

小结

总体感觉就是 Haskell 对副作用进行封装之后, 整个概念多了太多[br]特别对可变状态这种东西, 也许得说是共享的可变状态, 这种结构, 太不明确[br]但是应用开发当中遇到可变状态是稀松平常的事情(高端的数据库后端就...)[br]Haskell 宣称自己适合实际开发, 我们也确实需要很多 Haskell 的特性[br]但是这么多限制累积在一起成了那么高的门槛, 我还是先折腾 Clojure 吧

Anyway, 通过 uglify 而且 tricky 的手法, 可变状态也能模拟[br]至少我现在知道了 Haskell 并不是完全把这个路给堵死了

关键字:haskell, clojure

版权声明

本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部