Functor and Monad in Swift
I have been trying to teach myself Functional Programming since late 2013. Many of the concepts are very daunting because of their somewhat academic nature.
Since I’m obviously not an expert, I intend this to be a very practical post. You will find many posts trying to explain what a Monad is, some of them trying a bit too hard to come up with similes, but hopefully the sample code here will illustrate some of the concepts better.
It wasn’t until recently that I finally could say that I got what Monad means. Let’s explore why this concept even exists, and how it can help you when writing Swift code.
Map
One of the first things that we got to see at the 2014 WWDC with the introduction of Swift was that we could use the map
function with the collection types. Let’s focus on Swift’s Array
.
let numbers = [1, 2, 3] let doubledNumbers = numbers.map { $0 * 2 }
// doubledNumbers: 2, 4, 6
The benefit of this pattern is that we can very clearly express the transformation that we’re trying to apply on the list of elements (in this case, doubling their value). Compare this with the imperative approach:
var doubledImperative: [Int] = []
for number in numbers {
doubledImperative.append(number * 2)
}
// doubledImperative: 2, 4, 6
It’s not about solving it in a one-liner vs 3 lines, but with the former concise implementation, there’s a significantly higher signal-to-noise ratio. map
allows us to express what we want to achieve, rather than how this is implemented. This eases our ability to reason about code when we read it.
But map
doesn’t only make sense on Array
. map
is a higher-order function that can be implemented on just any container type. That is, any type that, one way or another, wraps one or multiple values inside.
Let’s look at another example: Optional
. Optional
is a container type that wraps a value, or the absence of it.
let number = Optional(815) let transformedNumber = number.map { $0 * 2 }.map { $0 % 2 == 0 }
// transformedNumber: Optional.Some(true)
The benefit of map
in Optional
is that it will handle nil values for us. If we’re trying to operate on a value that may be nil
, we can use Optional.map
to apply those transformations, and end up with nil
if the original value was nil
, but without having to resort to nested if let
to unwrap the optional.
let nilNumber: Int? = .None let transformedNilNumber = nilNumber.map { $0 * 2 }.map { $0 % 2 == 0 }
// transformedNilNumber: None
From this we can extrapolate that map
, when implemented on different container types, can have slightly different behaviors, depending on the semantics of that type. For example, it only makes sense to transform the value inside an Optional
when there’s actually a value inside.
This is the general signature of a map
method, when implemented on a Container
type, that wraps values of type T
:
func map<U>(transformFunction: T -> U) -> Container<U>
Let’s analyze that signature by looking at the types. T
is the type of elements in the current container, U
will be the type of the elements in the container that will be returned. This allows us to, for example, map an array of strings, to an array of Int
s that contains the lengths of each of the String
s in the original array.
We provide a function that takes a T
value, and returns a value of type U
. map
will then use this function to create another Container
instance, where the original values are replaced by the ones returned by the transformFunction
.
Implementing map
with our own type
Let’s implement our own container type. A Result
enum is a pattern that you will see in a lot of open source Swift code today. This brings several benefits to an API when used instead of the old Obj-C NSError-by-reference argument.
We could define it like this:
enum Result<T> {
case Value(T)
case Error(NSError)
}
This is an implementation of a type known as Either
in some programming languages. Only in this case we’re forcing one of the types to be an NSError
instead of being generic, since we’re going to use it to report the result of an operation.
Conceptually, Result
is very similar to Optional
: it wraps a value of an arbitrary type, that may or may not be present. In this case, however, it may additional tell us why the value is not there.
To see an example, let’s implement a function that reads the contents of a file and returns the result as a Result
object:
func dataWithContentsOfFile(file: String, encoding: NSStringEncoding) -> Result<NSData> {
var error: NSError? if let data = NSData(contentsOfFile: file, options: .allZeros, error: &error) {
return .Value(data)
}
else {
return .Error(error!)
}
}
Easy enough. This function will return either an NSData
object, or an NSError
in case the file can’t be read.
Like we did before, we may want to apply some transformation to the read value. However, like in the case before, we would need to check that we have a value every step of the way, which may result in ugly nested if let
s or switch
statements. Let’s leverage map
like we did before. In this case, we will only want to apply such transformation if we have a value. If we don’t, we can simply pass the same error through.
Imagine that we wanted to read a file with string contents. We would get an NSData
, that then we need to transform into a String
. Then say that we want to turn it into uppercase:
NSData -> String -> String
We can do this with a series of map
transformations (we’ll discuss the implementation of map
later):
let data: Result<NSData> = dataWithContentsOfFile(path, NSUTF8StringEncoding) let uppercaseContents: Result<String> = data.map { NSString(data: $0, encoding: NSUTF8StringEncoding)! }.map { $0.uppercaseString }
Similar to the early example with map
on Array
s, this code is a lot more expressive. It simply declares what we want to accomplish, with no boilerplate.
In comparison, this is what the above code would look like without the use of map
:
let data: Result<NSData> = dataWithContentsOfFile(path, NSUTF8StringEncoding) var stringContents: String? switch data {
case let .Value(value):
stringContents = NSString(data: value, encoding: NSUTF8StringEncoding)
case let .Error(error):
break
} let uppercaseContents: String? = stringContents?.uppercaseString
How would Result.map
be implemented? Let’s take a look:
extension Result {
func map<U>(f: T -> U) -> Result<U> {
switch self {
case let .Value(value):
return Result<U>.Value(f(value))
case let .Error(error):
return Result<U>.Error(error)
}
}
}
Again, the transformation function f
takes a value of type T
(in the above example, NSData
) and returns a value of type U
(String
). After calling map
, we’ll get a Result<U>
(Result<String>
) from an initial Result<T>
(Result<NSData>
). We only call f
whenever we start with a value, and we simply return another Result
with the same error otherwise.
Functors
We’ve seen what map
can do when implemented on a container type, like Optional
, Array
or Result
. To recap, it allows us to get a new container, where the value(s) wrapped inside are transformed according to a function. So what’s a Functor you may ask? A Functor is any type that implements map
. That’s the whole story.
Once you know what a functor is, we can talk about some types like Dictionary
or even closures, and by saying that they’re functors, you will immediately know of something you can do with them.
Monads
In the earlier example, we used the transformation function to return another value, but what if we wanted to use it to return a new Result
object? Put another way, what if the transformation operation that we’re passing to map
can fail with an error as well? Let’s look at what the types would look like.
func map<U>(f: T -> U) -> Result<U>
In our example, T
is an NSData
that we’re converting into U
, a Result<String>
. So let’s replace that in the signature:
func map(f: NSData -> Result<String>) -> Result<Result<String>>
Notice the nested Result
s in the return type. This is probably not what we’ll want. But it’s OK. We can implement a function that takes the nested Result
, and flattens it into a simpleResult
:
extension Result {
static func flatten<T>(result: Result<Result<T>>) -> Result<T> {
switch result {
case let .Value(innerResult):
return innerResult
case let .Error(error):
return Result<T>.Error(error)
}
}
}
This flatten
function takes a nested Result
with a T
inside, and return a single Result<T>
simply by extracting the inner object inside the Value
, or the Error
.
A flatten
function can be found in other contexts. For example, one can flatten
an array of arrays into a contiguous, one-dimensional array.
With this, we can implement our Result<NSData> -> Result<String>
transformation by combining map
and flatten
:
let stringResult = Result<String>.flatten(data.map { (data: NSData) -> (Result<String>) in
if let string = NSString(data: data, encoding: NSUTF8StringEncoding) {
return Result.Value(string)
}
else {
return Result<String>.Error(NSError(domain: "com.javisoto.es.error_domain", code: JSErrorCodeInvalidStringData, userInfo: nil))
}
})
This is so common, that you will find this defined in many places as flatMap
or flattenMap
, which we could implement for Result
like this:
extension Result {
func flatMap<U>(f: T -> Result<U>) -> Result<U> {
return Result.flatten(map(f))
}
}
And with that, we turned our Result
type into a Monad! A Monad is a type of Functor. A type which, along with map
, implements a flatMap
function (sometimes also known asbind
) with a signature similar to the one we’ve seen here. Container types like the ones we presented here are usually Monads, but you will also see that pattern for example in types that encapsulate deferred computation, like Signal
or Future
.
The words Functor and Monad come from category theory, with which I’m not familiar at all. However, there’s value in having names to refer to these concepts. Computer scientists love to come up with names for things. But it’s those names that allow us to refer to abstract concepts (some extremely abstract, like Monad), and immediately know what we mean (of course, assuming we have the previous knowledge of their meaning). We get the same benefit out of sharing names for things like design patterns (decorator, factory…).
It took me a very long time to assimilate all the ideas in this blog post, so if you’re not familiar with any of this I don’t expect you to finish reading this and immediately understand it. However, I encourage you to create an Xcode playground and try to come up with the implementation for map
, flatten
and flatMap
for Result
or a similar container type (perhaps try with Optional
or even Array
), and use some sample values to play with them.
And next time you hear the words Functor or Monad, don’t be scared :) They’re simply design patterns to describe common operations that we can perform on different types.
Open source version of the article, where you can create an issue to ask a question or open pull requests: https://github.com/JaviSoto/Blog-Posts/blob/master/Functor%20and%20Monad%20in%20Swift/FunctorAndMonad.md
http://www.javiersoto.me/post/106875422394
Functor and Monad in Swift的更多相关文章
- 函数编程中functor和monad的形象解释
函数编程中functor和monad的形象解释 函数编程中Functor函子与Monad是比较难理解的概念,本文使用了形象的图片方式解释了这两个概念,容易理解与学习,分别使用Haskell和Swift ...
- 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad
经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...
- 怎样理解Functor与Monad
1. 复合函数操作符 Prelude> :t (.) (.) :: (b -> c) -> (a -> b) -> a -> c Prelude> (.) ( ...
- 重新理解 Monad
对于大多数刚刚入门函数式编程的同学来说,monad(单子.又叫单体)可能是这里面的一道坎.你可能对 map . flatMap 以及 filter 再熟悉不过,可是到了高阶的抽象层次上就又会变得一脸懵 ...
- Monad / Functor / Applicative 浅析
前言 Swift 其实比 Objective-C 复杂很多,相对于出生于上世纪 80 年代的 Objective-C 来说,Swift 融入了大量新特性.这也使得我们学习掌握这门语言变得相对来说更加困 ...
- 函数式编程-将Monad(单子)融入Swift
前言 近期又开始折腾起Haskell,掉进这个深坑恐怕很难再爬上来了.在不断深入了解Haskell的各种概念以及使用它们去解决实际问题的时候,我会试想着将这些概念移植到Swift中.函数式编程范式的很 ...
- Functor、Applicative 和 Monad(重要)
Functor.Applicative 和 Monad Posted by 雷纯锋Nov 8th, 2015 10:53 am Functor.Applicative 和 Monad 是函数式编程语言 ...
- Functor、Applicative 和 Monad
Functor.Applicative 和 Monad 是函数式编程语言中三个非常重要的概念,尤其是 Monad. 说明:本文中的主要代码为 Haskell 语言,它是一门纯函数式的编程语言. 一.结 ...
- swift 学习(二)基础知识 (函数,闭包,ARC,柯里化,反射)
函数 func x(a:Int, b:Int) {} func x(a:Int, b:Int) -> Void {} func x(a:Int, b:Int) ->(Int,Int ...
随机推荐
- 【[Offer收割]编程练习赛12 C】矩形分割
[题目链接]:http://hihocoder.com/problemset/problem/1495 [题意] [题解] 把每个方块都再分成3*3的小块; 这样; 对于一个方块来说 如果是'\' 则 ...
- [luoguP3275] [SCOI2011]糖果(差分约束)
传送门 差分约束裸题 但是坑! 有一个点是长为10W的链,需要逆序加边才能过(真是玄学) 还有各种坑爹数据 开longlong ——代码 #include <cstdio> #includ ...
- noip模拟赛 fateice-or
分析:or操作只有在结果的这一位为0的情况下才会强制要求两个数的这一位都为0,其它时候不强求,所以为了最大限度地满足条件,我们先把所有的数的所有位全部变成1,如果p的第i位为0,那么[l,r]的数的第 ...
- oracle导入少量数据(少于10M)
工具用PL/SQL Developer select * from temp1 for update;开锁,点+号,直接从Excel复制,然后粘贴. 程序猿必读
- jmeter默认生成测试报告
我的jmeter安装在F:\study\apache-jmeter-3.1\apache-jmeter-3.1下,在bin目录下执行 其中E:\code\jmeterReport\Course-tes ...
- Git使用SSH提交代码到server出现 permission denied (publickey).
在GitBush中向已经存在的Repository提交README.md改动. 命令例如以下: touch README.md git init git add README.md git commi ...
- 支付宝即时到帐接口的python实现,演示样例採用django框架
因工作须要研究了支付宝即时到帐接口.并成功应用到站点上,把过程拿出来分享. 即时到帐仅仅是支付宝众多商家服务中的一个,表示客户付款,客户用支付宝付款.支付宝收到款项后,立即通知你,而且此笔款项与交易脱 ...
- Ubuntu下终端Vim编写C语言程序 AAAAA
我是开虚拟机下的Ubuntu,装双系统又卸了,Ubuntu默认是不包含编辑器vim和编译器gcc.如果你是刚安装好的Ubuntu电脑,下面我们将来实现自己的第一个程序. 1.准备工作 首先进入root ...
- LESS2CSS for sumlime text2
Windows下的安装 Less2Css插件依赖lessc这个工具,在windows下可以下载或者用git cloneless.js-windows到本地目录.然后把目录地址加入到环境变量PATH的中 ...
- PCB MS SQL 通过表名查询各字段信息和vb.net C# module类代码
正式表:各字段内容获取 ) SET @tabname = 'ppeflow' SELECT @tabname AS '表名' ,(CASE ))+ ')' )) ) )) + ')' )) ) )) ...