Skip to content

Instantly share code, notes, and snippets.

@mathonsunday
Created December 8, 2015 23:06
Show Gist options
  • Select an option

  • Save mathonsunday/304baf3df67ecaf97660 to your computer and use it in GitHub Desktop.

Select an option

Save mathonsunday/304baf3df67ecaf97660 to your computer and use it in GitHub Desktop.
//: Playground - noun: a place where people can play
import Cocoa
import Foundation
// WinterBuddies a social network to connect you with people who can help you survive the winter
class ProfileViewController : UIViewController {
let musicService = MusicService()
init(musicService: MusicService) {
self.musicService = musicService
}
override func viewDidLoad() {
super.viewDidLoad()
musicService.play() // start playlist on application
}
}
// ProfileViewControllerTests.swift
func testShouldPlayMusicWhenViewDidLoad() {
// setup mock
class MockMusicService : MusicService {
var musicPlayed = false
override func play() {
musicPlayed = true
}
}
var viewController = ProfileViewController(musicServicer: MockMusicService())
// invoke
viewController.viewDidLoad()
// verify
XCTAssertTrue(mockMusicService.playCalled)
}
class MockMusicService: MusicService {
class func basic() -> MusicService {
return builder().build()!
}
class func builder() -> MusicServiceBuilder {
return MusicServiceBuilder()
.withApplication("Spotify")
.withPlaylist("Chill Party")
}
}
extension MusicServiceBuilder {
func withApplication(application: String) -> MusicServiceBuilder {
self.application = application
return self
}
func withPlaylist(playlistName: String) -> MusicServiceBuilder {
self.playlistName = playlistName
return self
}
}
@mpurland
Copy link

mpurland commented Dec 8, 2015

Reference: https://gist.github.com/mathonsunday/a4ec3b5131a46b902129#file-reader-swift-L11
Monad Reader: https://github.com/typelift/Swiftz/blob/reader-monad/Swiftz/Reader.swift

Line 30: Assuming the MusicService does not have any dependencies I would think about it as a Reader<Void, MusicService> so that it might become:

let musicReader: Reader<Void, MusicService>

var viewController = ProfileViewController(musicService: musicReader.ask()())

@mpurland
Copy link

mpurland commented Dec 9, 2015

According to http://blog.originate.com/blog/2013/10/21/reader-monad-for-dependency-injection/:

This could be done this way (let me know what you think):

protocol MusicService {
   func getArtist(id: String) -> Artist
   var playing: Bool { get }
}
protocol Config {
   var musicService: MusicService
}

struct MyConfig: Config {
   let musicService: MusicService = AwesomeMusicService()
}

struct MockConfig: Config {
   let musicService: MusicService = MockMusicService()
}

protocol Artists {
   func getArtist(id: String) -> Reader<Config, Artist>
}

extension Artists {
   func getArtist(id: String) -> Reader<Config, Artist> {
      return Reader { self.getArtistReader($0) }
   }
   func getArtistReader(config: Config) -> String -> Artist {
      return config.musicService.getArtist
   }
}

struct Application {
}

extension Application: Artists {
}

let app = Application()

let getArtistReader: Reader<Config, Artist> = app.getArtist("1") // from some config, reader type defined for clarity

let artist1 =  getArtistReader.runReader(MyConfig())
let artist2 = getArtistReader.runReader(MockConfig())

Something along these lines I think.

@mathonsunday Now to explore on how to get it into the UIViewController

@mpurland
Copy link

mpurland commented Dec 9, 2015

The last can also be done:

let artist1 =  getArtistReader >>- { $0.runReader(MyConfig()) }
let artist2 = getArtistReader >>- { $0.runReader(MockConfig()) }

@mathonsunday
Copy link
Author

Your code makes sense to me. That article has also been what I've been using for reference.

In the ProfileViewController I want to get the MusicService from MyConfig and in the ProfileViewControllerTest I want to get the MusicService from mockConfig. This would be standard constructor injection and wouldn't take advantage of the Reader monad.

To better utilize the Reader monad I could modify my example to just play music for the user's favorite Artist. However I still don't see how my approach would be better than traditional DI since I'm not "pushing the injection out to the edges of our application where it belongs."

Chris Eidhof commented on this: I think a Reader monad makes sense in Haskell, but not necessarily in Swift. Because we would have to put everything inside a flatMap (or bind instead of flatMap). Otherwise it’s not really a reader monad anyway. The reason to use it in Haskell is to share state across a computation (and also allow for local state.) However, because we don’t have do notation in Swift it becomes really cumbersome.

We have:

// WinterBuddies a social network to connect you with people who can help you survive the winter
class ProfileViewController : UIViewController {
    let musicService = MyConfig().musicService
    // normally I would just get the favoriteArtist from the music service
    let favoriteArtist = Application().getArtistReader.runReader(MyConfig())
    init(musicService: MusicService, favoriteArtist: Artist) {
        self.musicService = musicService
        self.favoriteArtist = favoriteArtist
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        musicService.play(favoriteArt) // start playing favorite artist on application 
    }
}

// ProfileViewControllerTests.swift
func testShouldPlayMusicWhenViewDidLoad() {
    // setup mock
    class MockMusicService : MusicService {
        var musicPlayed = false
        override func play() {
            musicPlayed = true
        }
    }

    var viewController = ProfileViewController(musicService: MyMockConfig().musicService)

    // invoke
    viewController.viewDidLoad()

    // verify
    XCTAssertTrue(mockMusicService.playCalled)
}

@mpurland
Copy link

I tend to agree that the Reader monad may provide less value than in Haskell or Scala because of the lack of syntax support in Swift. Haskell do and Scala for-comprehension notation is great for this use case. I think this would also depend on the use case your going to use it in Swift. I'm not an expert, but it's definitely interesting to think about.

If you want constructor injection of only a single type then it may make sense to use simple constructor injection. I think the power of the Reader monad is about the idea that it represents. Given an environment E it will output the modified environment A. It's essentially a function E -> A. If we create our MusicService reader that takes in the environment Config and returns a MusicService.

let musicServiceReader = Reader<Config, MusicService> 

This would allow the Config to be injected anywhere as a dependency while allowing computations on the monad to be performed without extracting the Config state from the Reader. As far as I understand it this is possible by implementing fmap, apply, and flatMap (bind >>-) for the monad in question, the Reader monad. The series of computations are performed without necessarily unwrapping the environment as context from the Reader.

Then if we change the definition slightly:

let configReader = Reader<Void, Config>
let musicReader = configReader.fmap { $0.musicService } // musicReader is of type Reader<Void, MusicService>

This might allow the configReader to be passed in anywhere it's needed but still allow the extraction of the needed environment while applying operations to it like a monad. This would allow the setup and logic for the particular Reader or Config in this case to be encapsulated and separate from the rest of the control flow.

The example might then be:

protocol MusicService {
    func play(artist: Artist)
}

class AwesomeMusicService: MusicService {
    func play(artist: Artist) {
        print("artist: \(artist) brought to you buy Radio AMS.")
    }
}

class MockMusicService: MusicService {
    var lastArtistPlayed: Artist? = nil

    init() {
    }

    func play(artist: Artist) {
        lastArtistPlayed = artist
    }
}

struct Artist {
    let name: String
}

protocol Config {
    var musicService: MusicService { get }
    var favoriteArtist: Artist { get }
}

struct MyConfig: Config {
    var musicService: MusicService
    var favoriteArtist: Artist

    init() {
        musicService = AwesomeMusicService()
        favoriteArtist = Artist(name: "Ferry Corsten")
    }
}

struct MockConfig: Config {
    var musicService: MusicService
    var favoriteArtist: Artist

    init() {
        musicService = MockMusicService()
        favoriteArtist = Artist(name: "Mock")
    }
}

func musicPlayFavoriteArtist(config: Config) {
    config.musicService.play(config.favoriteArtist)
}

class ProfileViewController : UIViewController {
    let configReader: Reader<Void, Config>
    init(configReader: Reader<Void, Config>) {
        self.configReader = configReader
    }
    override func viewDidLoad() {
        super.viewDidLoad()

        musicPlayFavoriteArtist <^> configReader >>- runReader
    }
}

/// Some standalone examples
let mockConfigReader = Reader<Void, Config>.pure(MockConfig())
musicPlayFavoriteArtist <^> mockConfigReader >>- runReader
// Played "Mock"

let myConfigReader = Reader<Void, Config>.pure(MockConfig())
musicPlayFavoriteArtist <^> myConfigReader >>- runReader
// Played "Ferry Corsten"

@mathonsunday Let me know if this makes sense or needs clarification.

@mathonsunday
Copy link
Author

Not clear to me why this is desirable

The series of computations are performed without necessarily unwrapping the environment as context from the Reader.

Does it necessarily lead to

This would allow the setup and logic for the particular Reader or Config in this case to be encapsulated and separate from the rest of the control flow.

So, for the test I'd do:

// ProfileViewControllerTests.swift
func testShouldPlayMusicWhenViewDidLoad() {

    let mockConfigReader = Reader<Void, Config>.pure(MockConfig())
    var viewController = ProfileViewController(configReader)

    // invoke
    viewController.viewDidLoad()

    // verify
    XCTAssertTrue(mockConfigReader.musicPlayFavoriteArtist) // our approach might not jibe well with this 
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment