在Swift中实现Haskell风格的Monads。
作为了解Monads的一种方式,我开始在Swift中实现几个Monads。第一个是IO Monads,几乎完成。我希望随着时间的推移实现其他Monads。状态、Writer和Maybe(本质上等同于Optional
)的想法都在我的脑海中。
每个Monads都将定义所有的Functor和Applicative类型类操作。
开玩笑的。我开始爱上Monads、范畴理论和函数式编程,但不是我来解释它们的人。写自己的Monads版本帮助我理解了很多,但我知道我所知道的是相当肤浅的。代替我自己版本的Monads-are-burritos教程,我将仅分享一些帮助我的资源链接。
有什么想法应该包括在内?创建一个带有链接的问题,我会看一下。
坦白说,关于Monads、范畴理论和类似的想法,直到我开始写自己的版本,这些想法才在我心中逐渐变得一致。列表、自记录的Writer函数、Maybe值、IO操作和状态转换有什么共同之处?它们都看起来如此不同。但它们都共享一个共同的数学基础,这是我刚开始对我清晰起来的。
大多数 Haskell 的标准运算符已经被 Swift 标准库定义,但为了其他用途。例如,在 Haskell 中,>>=
是 bind
运算符,>>
是 sequence
运算符。Swift 已经为位运算操作定义了这两个运算符。为了避免与标准库冲突,我不得不使用不与标准 Haskell 版本匹配的运算符。这是件不幸的事情,主要是因为没有人喜欢学习一整套新的运算符,但事情就是这样。
下面是他们如何翻译的
Haskell LVGMonads Name Definition
--------------------------------------------------------------------------------------------
<$> <^> fmap (<^>) :: Functor f => (a -> b) -> f a -> f b
<*> <*> apply (<*>) :: Applicative f => f (a -> b) -> f a -> f b
>>= =>> bind (=>>) :: Monad m => m a -> (a -> m b) -> m b
>> ->> N/A (->>) :: Monad m => m a -> m b -> m b
还有另一组运算符不是特定于类型类函数,但被库用于函数组合和应用
Haskell LVGMonads Definition
---------------------------------------------------------------------
. .<< (.<<) :: (b -> c) -> (a -> b) -> (a -> c)
N/A .>> (.>>) :: (a -> b) -> (b -> c) -> (a -> c)
$ <-- (<--) :: (a -> b) -> a -> b
N/A --> (-->) :: a -> (a -> b) -> b
.<<
和 .>>
分别是右到左的函数组合和左到右的函数组合。而 <--
和 -->
分别是右到左的函数应用和左到右的函数应用。
如果你想指责我强迫症,我确实让所有这些运算符的长度都是 3 个字符,因为 Xcode 内部代码块缩进是 4 个空格。这意味着如果你用其中一个运算符开始一行,然后是一个空格再跟一个表达式,你的代码会自然对齐。
aitch
.<< gee
.<< eff
<-- ex // evaluates to aitch(gee(eff(ex)))
其中一些运算符可以更短,但那样会导致空格错位,世界就会毁灭。我的方法中有一丝疯狂。
可以为每个特定的 Monad 定义额外的运算符,但上面列出的在这些 Monad 中都是通用的。
IO 单调表示一个产生副作用的行为。它是表示用户输入、UI 更新、读取和写入文件等的方式。它要么从外部世界接收数据,要么以某种(希望是有意义的方式)改变世界。
我尝试从 Haskell 中实现了两个主要思想:1) IO 行为在其定义时不会执行 - 执行被延迟,直到它们被父 IO 函数调用;2) 不应该在任何地方都能执行 IO 行为 - 应该有一个父 IO 行为调用所有其他的 IO 行为。第一个想法很容易实现 - 取行动并且将其包装在一个可以在以后执行的闭包中(示例将随后给出)。第二个想法稍微困难一些。
在 Haskell 中只有一个 IO 函数可以调用 -那就是 main
函数。main
启动程序,程序中的任何其他返回 IO 类型的函数都必须由 main
调用,而 main
本身是一个函数,它总是返回 IO ()
。在 Haskell 中,这些规则是通过语言执行的,它没有调用除 main
之外任何 IO 函数的机制(如果我对 Haskell 的了解有误,请纠正我 - 我还有很多关于 Haskell 要学习)。
在 Swift 中没有方法来强制执行这样的规则。我提出了一种破解方法,使你不得不费尽周折地执行 IO 行为 - 我使只有一种类型的 IO 行为可以被执行:IO<Main>
。而 Main
本身只是一个虚构的类型
public struct Main { public init() { } }
所以Swift中Haskell的main:: IO ()
等价于let main: IO
。当然,与Haskell不同,您可以在Swift代码的任何地方放置一个IO
操作,并根据需要执行它。从某种意义上说,每个小小的IO
操作都是プログラムとして独立存在的,这有点酷。毕竟,人们不太可能将整个iPhone应用程序作为一个单独的IO
操作来重新编写。但他们可能会发现这里或那里使用IO类型的Monadic结构是有意义的。这些Monadic结构的组合性质使得可能从一个非常小的开始,自然地增长到一个非常大的东西。
IO类型非常简单,就是这样
public struct IO<A> {
/// The IO action to perform.
let action: () -> A
/// Initialize an IO object with a closure that contains an IO action.
public init(_ action: () -> A) {
self.action = action
}
}
可以将IO操作(警告:过于简化)分为两大类 - 读操作和写操作。读操作的例子可以是获取标准输入的一行。在Swift中,您可以使用readLine()
函数来完成这项任务,该函数返回一个String?
。我们可以将其转换为IO操作(我们现在简单地将它强制转换为一个非可选的String
)
let ioReadLine: IO<String> = IO { readLine()! }
现在让我们创建一个写操作。我们将print
一些到标准输出去
let hello: IO<()> = { print("Hello world!") }
hello
没问题,但它只会打印一个东西。让我们将其推广为一个函数,该函数接受一个String
作为输入,并返回一个将打印该String
的IO操作
let ioPrint: String -> IO<()> = { s in IO { print(s) } }
现在我们可以通过给ioPrint
输入String
来创建IO<()>
操作
let startTheRumpus = ioPrint("Let the wild rumpus start!")
IO操作是可组合的。可以从小额开始构建大量的东西。有两种操作用于组合IO操作,=>>
和->>
。
第一个是=>>
,发音为bind
,它在Haskell中等同于>>=
操作符。这里是其定义
public func =>> <A, B> (ioa: IO<A>, f: A -> IO<B>) -> IO<B>
它接受一个IO
和一个类型为A -> IO
的函数,返回一个IO
。让我们使用它来结合ioReadLine
(一个IO
)和ioPrint
(一个类型为String -> IO<()>
的函数)
let echo: IO<()> = ioReadLine =>> ioPrint
现在,每当echo
执行时,它将从标准输入读取一行,并将得到的String
传入ioPrint
。
注意,echo
也可以这样写
let echo: IO<()> = ioReadLine =>> { x in ioPrint(x) }
这种第二种技术,如果您需要在打印之前对x
进行更复杂的操作时很有用
let echo: IO<()> = ioReadLine =>> { x in ioPrint(x.uppercaseString) }
用于组合IO操作的第二个操作符是->>
。它本身没有名字,但我喜欢称之为then
,所以a ->> b
可以读作a then b
。它与Haskell中的>>
操作符相同。与=>>
一样,它将两个IO操作组合成一个新的,但它忽略了第一个IO操作的结果。
例如,echo
很好,但需要在用户输入文本到终端之前提示一行文本。让我们使用ioPrint
在调用echo
之前提示用户输入
let betterEcho: IO<()> = ioPrint("Please enter some text:") ->> echo
单独看,无法执行这些操作。只有类型的IO
的IO操作才能执行。那么我们如何调用它们呢?像这样
// This defines our main function:
let main: IO<Main> = ioReadLine =>> ioPrint =>> exit
这里发生了什么?简而言之,ioReadline
从标准输入读取一个String
。这个值被喂入到ioPrint
。 ioPrint
返回一个IO<()>
动作,当它执行时返回一个()
。将这个()
传入到全局特殊函数exit
,其类型为() -> IO
。因此,当exit
从ioPrint
接收输入()
后,它返回IO
。
但是,这一切并不是立即发生的。我们到现在为止已经将一些IO动作组合成了单个IO动作,并将其命名为main
。直到我们执行main
, nothing才会发生,我们可以使用方便的<=
前缀运算符来执行它。
// Execute the main function:
<=main
只有当main
被执行时,其他动作才会按照它们组合的顺序执行。
一旦至少有一个Monad(IO将是第一个)准备好发布,它将在CocoaPods上可用。目前,如果您想试试它,最好的办法是克隆仓库并将其源文件包含到您的项目中。它们很小。
letvargo,[email protected]
LVGMonads可在MIT许可下使用。有关更多信息,请参阅LICENSE文件。