神刀安全网

Understand Monads with this One Weird Trick

About the Speaker: Andy Bartholomew

Andy Bartholomew is an iOS developer at Airbnb. He loves working with static type systems and thinking about how to make code more readable. In his spare time, he likes to read comics, cook new recipes, and play with his cat, Mei.

@jqsilver

The Trick is to Translate Haskell Code into Swift

About two years ago, I was going to quit iOS development. Then Swift was announced. I used Swift to rebuild the entire app of the company I was working for at the time. When Swift first came out, I noticed articles writing about the concepts of monads and functors, and I eventually learned that they’re concepts from Haskell. Haskell is difficult to read, so I started translating code into Swift with descriptive naming for variables, and it helped me understand everything a lot better.

The first thing to understand is that Haskell has this notion of type classes, which is written in the syntax as just class . This is actually a protocol in Swift, equivalent to a protocol .

Monad (A Monad is a Monoid in the Category of Endofunctors)

class Monoid m where   mempty :: m   mappend :: m -> m -> m   mconcat :: [m] -> m   mconcat = foldr mappend mempty

Here’s the definition of a monoid in Haskell. The m here is the concrete type that adheres to the class or implements the protocol.

  • mempty returns that value of type m . It should be of the minimal type, a zero or an empty list.

  • mappend takes two values and combines them into one value.

  • concat takes a list of values and combines them all.

  • foldr is basically reduce

The Swift equivalent:

protocol Monoid {   static func empty() -> m   static func append(m, m) -> m   static func concat([m]) -> m }

The value m is the thing that implements the protocol. To translate this into Swift, we’ll use Self , which does the same thing.

protocol Monoid {   static func empty() -> Self   static func append(Self, Self) -> Self   static func concat([Self]) -> Self }

protocol Monoid {   static func empty() -> Self   static func append(left: Self, right: Self) -> Self   static func concat(list: [Self]) -> Self }

The naming here is a bit obvious, but nonetheless, it’s easier to understand compared to the original version. Here, we added left , right , and list .

protocol Monoid {   static func makeEmpty() -> Self   static func concat(left: Self, right: Self) -> Self   static func concat(list: [Self]) -> Self }

Note that the append function has now been renamed to concat . append is a misnomer because it suggests a mutation of an object. Here, we’re merely just taking two objects and combining them to produce a new object.

The Swift API guidelines suggests that programmers should avoid obscure terms. Moreover, it also says protocols that describes a capability should be named using the suffix ‘-able’.

Monoid means Concattable

Monoid is really just a protocol called “concattable” for defining how things can be concatenated with each other. For the purpose of making this more Swift-like, we can make the factory methods just an initializer, as it takes no arguments. The second function can just be an operator.

protocol Concattable {   init()   func +(left: Self, right: Self) -> Self   static func concat(list: [Self]) -> Self }

To note, operators defined in a protocol are actually static. This isn’t obvious because you don’t write static anywhere.

The interesting thing about understanding monoids, which we’re going to call concattable from now on, is that it’s more than just adding numbers. You can also concatenate two sets (a union of sets), you can concatenate two booleans, and overall make comparisons a lot more compact.

class Functor f where   fmap :: (a -> b) -> f a -> f b

A functor just has one method requirement called fmap . Cutting to the chase, we’ll name our Swift version Mappable .

protocol Mappable {   static func map(a -> b, f a) -> f b }

fmap works the same way as map works on arrays in Swift. Applying the same transformation from earlier, f is similar to m , which we can replace with Self , and we can also apply the naming conventions, resulting in the following:

protocol Mappable {   static func map(transform: a -> b, input: Self a) -> Self b }

What is f a in Haskell?

In Haskell, functions are called without parentheses. In the original Haskell implementation, f isn’t a function. Rather, it’s a parameterized type.

Parameterized types and generics are actually type constructors. For example, you see this when you can’t simply declare things as arrays in Swift without providing a type.

Parameterized types, aka generics

struct Array<Element> {...}  let x = Array // Invalid  let x = Array<Int>  protocol SequenceType {   associatedtype Element }

class Functor f where   fmap :: (a -> b) -> f a -> f b

protocol Mappable {   associatedtype Element   static func map<A, B>(transform: A -> B, input: Self<A>) -> Self<B> }

Renaming the parameterized types

I dislike T or U as parameterized types, so let’s call them InType and OutType , resulting in this:

class Functor f where   fmap :: (a -> b) -> f a -> f b

protocol Mappable {   associatedtype Element   static func map<InType, OutType>(transform: InType -> OutType, input: Self<InType>) -> Self<OutType> }

Input as instance (self)

To make it look more like the array’s map method, we have the input become the instance. This is almost exactly the same as sequence type with one big difference. The sequence type always returns an array when you map on it.

protocol SequenceType   typealias Element   func map<OutType>(transform: Element -> OutType) -> [OutType]

So, a functor is really just Mappable .

protocol Mappable {   associatedtype Element   func map<OutType>(transform: Element -> OutType) -> Self<OutType> }

Concrete implementations of mappable

extension Array: Mappable {   func map<OutType>(transform: Element -> OutType) -> [OutType] }  extension Optional: Mappable {   func map<OutType>(transform: Wrapped -> OutType) -> OutType? }  let maybeValue = maybeValue.map { $0.property } let maybeValue = maybeValue?.property

Here are some concrete implementations of Mappable . You may not know this, but Optional has a method called map on it, which does what I just described. This is something that you might use all the time if you’re programming in Swift, because it happens so often when you want to access a property on an optional that you have this ?. syntax. You can also view streams, an asynchronous array, and a promise as an optional. You can do this for trees, dictionaries, or other data structures.

Monads are a subtype of functor, with some extra abilities.

class Monad m where   return :: a -> m a   (>>=) :: m a -> (a -> m b) -> m b

The return takes a value and wraps it in the context, such as to wrap something in an optional, or to wrap something in an array.

The arrow thing >>= is read as “bind”. It takes a wrapped value and a transform (a function that expects an unwrapped value), and somehow makes it work. In the end, you get a different wrapped value.

Because >>= is read as “bind”, we should call a monad Bindable instead. In converting this into Swift, we’ll change the return to an initializer, and we convert the bind to an OutType.

protocol Bindable: Mappable {   init(element: Element)   static func bind<Out>(input: Self<In>, transform: Int -> Self<Out>) -> Self<Out> }

Optional is also Bindable, or optional is a monad.

extension Optional: Bindable {   associatedtype Element = Wrapped   func bind<Out>(transform: Wrapped -> Out?) -> Out? {     if let value = self {       return transform(value)     } else {       return nil     }   } }  let maybeResult = maybeValue.bind(someMethod)

Here, you can unwrap using if let and if it’s nil , just return nil . Otherwise, call transform on the value.

extension Array: Bindable {   func bind<Out>(transform: Element -> [Out]) -> [Out] {     let arrayOfArrays [[Out]] = self.map(transform)     return arrayOfArrays.flatten   } }

With arrays, the simplest thing is to call the transform on your self, which will give you an array of arrays. Then, you can just call flatten on that, and that concatenates all of them into one array.

This is actually flatMap in the standard library. So everytime you use flatMap , you’re actually using array as if it were a monad.

extension Array: Bindable {   func bind<Out>(transform: Element -> [Out]) -> [Out] {     return flatMap(transform)   } }

Optional chaining as monad

// property: OutType?  let result1: OutType?? = maybeObject.map { $0.property } let result2: OutType? = maybeObject.flatMap { $0.property } let result3= maybeObject?.property  let maybeValue: String? = maybeObject?.property?.stringValue

A ?. syntax is actually shorthand for calling flatMap . If that property is an optional type, calling map to access the property would give you the doubly wrapped optional, but, if you’re calling flatMap , then it gives you the regular one.

Renaming monad to Flatmappable

Let’s call monad Flatmappable , as it’s easier to understand. It has all the requirements of Mappable : you can put values into a context, and you can also apply values to function as if they weren’t in that context, as long as they end up in the context in that function.

The important thing is to remember that you’re using monads whenever you chain optionals, and also whenever you use guard. Monads also give you ways to chain passing optional arguments to functions, but there’s no special syntax for that, which is why you’re probably using guard.

The other point of monads is to think more abstractly about container types beyond arrays and optionals, to things like promises, streams, results, and all those other possible data structures, like trees and dictionaries.

Whenever you’re really confused, break things down. Tease out the bits and pieces that you understand, and translate difficult concepts into a language you can understand.

If you name it, you can tame it.It sounds goofy, but it’s true. If you can program something, hopefully that means you understand it. Program with intention, and give revealing variable names. Programming is communication, so translate your work into a language that is easier to communicate.

Naming matters.“Monad” is an offputting, confusing word, but “ flatMappable ” feels like something we can all deal with.

Q: Have you seen that Chris Lattner has a working implementation of passing in a generic associated type for a type? So soon, you should be able to do what you were wanting to do with the Self OutType.

Awesome! I haven’t read that, I should definitely read that. A lot of the things that I’ve used with Swift, and wished existed, have come to exist. That has been really encouraging.

See the discussion on Hacker News .

Get new videos & tutorials — we won’t email you for any other reason, ever.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Understand Monads with this One Weird Trick

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址