我最近看了 Haskell 的 Monad - State。我已经能够创建与这个 Monad 一起操作的函数,但是我试图将行为封装到一个类中,基本上我试图在 Haskell 中复制这样的东西:
class A {
public:
int x;
void setX(int newX) {
x = newX;
}
void getX() {
return x;
}
}
如果有人能提供帮助,我将不胜感激。谢谢!
我最近看了 Haskell 的 Monad - State。我已经能够创建与这个 Monad 一起操作的函数,但是我试图将行为封装到一个类中,基本上我试图在 Haskell 中复制这样的东西:
class A {
public:
int x;
void setX(int newX) {
x = newX;
}
void getX() {
return x;
}
}
如果有人能提供帮助,我将不胜感激。谢谢!
我首先要指出的是,Haskell 至少不鼓励传统的 OO 风格的开发。取而代之的是,它的特性和特性非常适合您在许多其他语言中找不到的那种“纯函数式”操作;简而言之,尝试从其他(传统语言)“引入”概念通常是一个非常糟糕的主意。
但我试图将行为封装到一个类中
因此,我想到的第一个主要问题是为什么?您肯定想对这个(传统的 OO 概念)类做点什么吗?
如果这个问题的近似答案是:“我想为某种数据结构建模”,那么你最好使用类似的东西
data A = A { xval :: Int }
> let obj1 = A 5
> xval obj1
5
> let obj2 = obj1 { xval = 10 }
> xval obj2
10
它演示了纯粹的、不可变的数据结构,以及“getter”函数和破坏性更新(使用记录语法)。这样,您可以根据需要将这些“数据构造”映射到新数据构造的函数组合来做任何需要做的工作。
现在,如果您绝对需要某种 State 模型(实际上,回答这个问题需要一些经验来确切地了解本地状态与全局状态是什么),那么只有这样您才会深入研究使用 State Monad,例如:
module StateGame where
import Control.Monad.State
-- Example use of State monad
-- Passes a string of dictionary {a,b,c}
-- Game is to produce a number from the string.
-- By default the game is off, a C toggles the
-- game on and off. A 'a' gives +1 and a b gives -1.
-- E.g
-- 'ab' = 0
-- 'ca' = 1
-- 'cabca' = 0
-- State = game is on or off & current score
-- = (Bool, Int)
type GameValue = Int
type GameState = (Bool, Int)
playGame :: String -> State GameState GameValue
playGame [] = do
(_, score) <- get
return score
playGame (x:xs) = do
(on, score) <- get
case x of
'a' | on -> put (on, score + 1)
'b' | on -> put (on, score - 1)
'c' -> put (not on, score)
_ -> put (on, score)
playGame xs
startState = (False, 0)
main = print $ evalState (playGame "abcaaacbbcabbab") startState
(无耻地从本教程中解脱出来)。注意在状态单子的上下文中使用类似的“纯不可变数据结构” ,除了“放置”和“获取”单子函数,它们有助于访问包含在状态单子中的状态。
最后,我建议你问问自己:你真正想用这个(OO)类模型完成什么?Haskell 不是您典型的 OO 语言,尝试将概念映射到 1 对 1 只会在短期(可能是长期)内让您感到沮丧。这应该是一个标准的口头禅,但我强烈建议从Real World Haskell一书中学习,其中作者能够深入研究选择任何一种工具或抽象而不是另一种工具的更详细的“动机”。如果你坚持,你可以在 Haskell 中对传统的 OO 结构进行建模,但我不建议这样做——除非你有充分的理由这样做。
将命令式代码转换为纯函数式上下文需要一些置换。
一个 setter 改变一个对象。由于懒惰和纯洁,我们不允许直接在 Haskell 中这样做。
也许,如果我们将 State monad 转录成另一种语言,它会更明显。你的代码是用 C++ 编写的,但是因为我至少想要垃圾收集,所以我会在这里使用 java。
由于 Java 从来没有定义匿名函数,首先,我们将为纯函数定义一个接口。
public interface Function<A,B> {
B apply(A a);
}
然后我们可以组成一个纯粹的不可变对类型。
public final class Pair<A,B> {
private final A a;
private final B b;
public Pair(A a, B b) {
this.a = a;
this.b = b;
}
public A getFst() { return a; }
public B getSnd() { return b; }
public static <A,B> Pair<A,B> make(A a, B b) {
return new Pair<A,B>(a, b);
}
public String toString() {
return "(" + a + ", " + b + ")";
}
}
有了这些,我们实际上可以定义 State monad:
public abstract class State<S,A> implements Function<S, Pair<A, S> > {
// pure takes a value a and yields a state action, that takes a state s, leaves it untouched, and returns your a paired with it.
public static <S,A> State<S,A> pure(final A a) {
return new State<S,A>() {
public Pair<A,S> apply(S s) {
return new Pair<A,S>(a, s);
}
};
}
// we can also read the state as a state action.
public static <S> State<S,S> get() {
return new State<S,S>() {
public Pair<S,S> apply(S, s) {
return new Pair<S,S>(s, s);
}
}
}
// we can compose state actions
public <B> State<S,B> bind(final Function<A, State<S,B>> f) {
return new State<S,B>() {
public Pair<B,S> apply(S s0) {
Pair<A,S> p = State.this.apply(s0);
return f.apply(p.getFst()).apply(p.getSnd());
}
};
}
// we can also read the state as a state action.
public static <S> State<S,S> put(final S newS) {
return new State<S,S>() {
public Pair<S,S> apply(S, s) {
return new Pair<S,S>(s, newS);
}
}
}
}
现在,在 state monad 中确实存在 getter 和 setter 的概念。这些被称为镜头。Java 中的基本表示形式如下所示:
public abstract class Lens[A,B] {
public abstract B get(A a);
public abstract A set(B b, A a);
// .. followed by a whole bunch of concrete methods.
}
这个想法是,镜头提供对知道如何从 A 中提取 B 的 getter 和知道如何获取 B 和一些旧 A 并替换 A 的一部分以产生新 A 的 setter 的访问。不能改变旧的,但它可以构造一个替换了其中一个字段的新的。
我在最近的波士顿地区 Scala 爱好者会议上发表了关于这些的演讲。您可以在此处观看演示文稿。
回到 Haskell,而不是在命令式的环境中谈论事情。我们可以进口
import Data.Lens.Lazy
来自comonad-transformers或此处提到的其他镜头库之一。该链接提供了必须满足才能成为有效镜头的法律。
然后你正在寻找的是一些数据类型,如:
data A = A { x_ :: Int }
带镜头
x :: Lens A Int
x = lens x_ (\b a -> a { x_ = b })
现在您可以编写如下代码
postIncrement :: State A Int
postIncrement = do
old_x <- access x
x ~= (old_x + 1)
return old_x
使用来自Data.Lens.Lazy的组合器。
上面提到的其他镜头库提供了类似的组合器。
首先,我同意 Raeez 的观点,这可能是错误的做法,除非你真的知道为什么!如果您想将某个值增加 42(例如),为什么不编写一个为您执行此操作的函数呢?
与传统的 OO 思维方式相比,传统的 OO 思维方式发生了很大变化,在这种思维方式中,您可以将它们取出、操作并将它们放回原处。我想说的是,直到您开始注意到这种模式“嘿!我总是把一些价值当作一个参数,最后返回它稍作修改,与其他一些值进行元组,所有管道都变得凌乱了!” 你真的不需要State
单子。(学习)Haskell 的部分乐趣在于寻找新的方法来绕过有状态的 OO 思维!
也就是说,如果您绝对想要一个带有x
of 类型的盒子Int
,您可以尝试制作自己的get
和put
变体,如下所示:
import Control.Monad.State
data Point = Point { x :: Int, y :: Int } deriving Show
getX :: State Point Int
getX = get >>= return . x
putX :: Int -> State Point ()
putX newVal = do
pt <- get
put (pt { x = newVal })
increaseX :: State Point ()
increaseX = do
x <- getX
putX (x + 1)
test :: State Point Int
test = do
x1 <- getX
increaseX
x2 <- getX
putX 7
x3 <- getX
return $ x1 + x2 + x3
现在,如果您runState test (Point 2 9)
在 ghci 中进行评估,您将返回(12,Point {x = 7, y = 9})
(因为 12 = 2 + (2+1) + 7 并且x
状态最后设置为 7)。如果你不关心返回的点,你可以使用evalState
,你会得到12
.
这里还有更高级的事情要做,比如使用类型类进行抽象Point
,以防你有多个数据结构,这些数据结构的行为类似于x
,但在我看来,最好留给另一个问题!