LVGMonads 1.0.1

LVGMonads 1.0.1

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最新发布2016年5月
SPM支持SPM

Aaron Rasmussen维护。



LVGMonads 1.0.1

  • letvargo

LVGMonads

在Swift中实现Haskell风格的Monads。

概览

作为了解Monads的一种方式,我开始在Swift中实现几个Monads。第一个是IO Monads,几乎完成。我希望随着时间的推移实现其他Monads。状态、Writer和Maybe(本质上等同于Optional)的想法都在我的脑海中。

每个Monads都将定义所有的Functor和Applicative类型类操作。

让我像别人从未解释过的一样解释Monads给你听

开玩笑的。我开始爱上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 单调

简介

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操作

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操作是可组合的。可以从小额开始构建大量的东西。有两种操作用于组合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

的IO操作才能执行。那么我们如何调用它们呢?像这样

// This defines our main function:
let main: IO<Main> = ioReadLine =>> ioPrint =>> exit

这里发生了什么?简而言之,ioReadline从标准输入读取一个String。这个值被喂入到ioPrintioPrint返回一个IO<()>动作,当它执行时返回一个()。将这个()传入到全局特殊函数exit,其类型为() -> IO

。因此,当exitioPrint接收输入()后,它返回IO

但是,这一切并不是立即发生的。我们到现在为止已经将一些IO动作组合成了单个IO动作,并将其命名为main。直到我们执行main, nothing才会发生,我们可以使用方便的<=前缀运算符来执行它。

// Execute the main function:
<=main

只有当main被执行时,其他动作才会按照它们组合的顺序执行。

安装

一旦至少有一个Monad(IO将是第一个)准备好发布,它将在CocoaPods上可用。目前,如果您想试试它,最好的办法是克隆仓库并将其源文件包含到您的项目中。它们很小。

要求

  • OS X 10.10或更高版本
  • iOS 8.0或更高版本

作者

letvargo,[email protected]

许可

LVGMonads可在MIT许可下使用。有关更多信息,请参阅LICENSE文件。