Skip to content

Instantly share code, notes, and snippets.

@sooop
Last active February 3, 2020 16:20
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save sooop/0206a746dbfeb6a54074 to your computer and use it in GitHub Desktop.
Save sooop/0206a746dbfeb6a54074 to your computer and use it in GitHub Desktop.
Monad in Swift : 모나드 개념을 Swift로 구현해본다.
/*
모나드는 특정한 타입을 감싸는 타입이며,
raw한 값을 감싸는 함수와
raw한 값을 모나드 값으로 바꾸는 어떤 함수에 바인딩된다.
이를 바탕으로 모나드 프로토콜을 정의하면 다음과 같다.
*/
protocol Monad {
typealias Element
static func _return_ (n:Self.Element) -> Self
func _bind_ (f:(Self.Element) -> Self) -> Self
}
/*
위 정의를 바탕으로 하스켈의 Maybe 타입을
흉내내어 본다.
이는 대수적으로는 Just A, Nothing 두 가지로 구분되므로
enum 타입이다.
*/
enum Maybe<T>: Monad, Printable {
typealias Element = T
case Just(Element)
case Nothing
// raw 한 값을 모나드값으로 만드는 함수 monad(_:)를
// 외부에서 별도 정의하는데, 이는 모나드 값을 만드는
// 아래 타입 메소드를 호출하게 된다.
static func _return_ (n:Maybe.Element) -> Maybe {
return .Just(n)
}
// 모나드 값을 함수에 바인딩하는 연산자 >>= 를 위한 함수
func _bind_ (f:(Element) -> Maybe) -> Maybe {
switch self {
case .Just(let a):
return f(a)
default:
return .Nothing
}
}
// println 문에 적용
var description: String {
switch self {
case .Just(let a):
return "Just \(a)"
default:
return "Nothing"
}
}
}
// 바인드 연산자
infix operator >>= { associativity left }
// 바인드 연산자는 모나드 타입 내 `_bind_` 메소드를 호출하고 끗.
func >>= <T, U:Monad where U.Element == T>(l:U, r:(T) -> U) -> U {
return l._bind_(r)
}
// raw 값을 모나드 값으로 바꾼다.
// 이 함수의 반환형은 좌변값의 타입에 의해 결정된다.
func monad <T, U:Monad where U.Element == T>(a:T) -> U {
return Maybe<T>._return_(a)
}
// 테스트용 함수 2개. 모나드에 바인딩될 수 있는 타입이다.
func increase(n:Int) -> Maybe<Int> {
return .Just(n + 1)
}
func fail(n:Int) -> Maybe<Int> {
return .Nothing
}
// 정수를 감싸는 모나드 타입 테스트
let a: Maybe<Int> = .Just(1)
let b = a >>= increase
let c = a >>= increase >>= increase
let d = a >>= increase >>= fail // .Nothing이 된다.
let e = a >>= increase >>= fail >>= increase
// ^~~~~ 여기서 .Nothing이 되었으므로 이후의
// 모든 바인딩에 대해서는 아무련 효력이 없다.
println([a, b, c, d, e])
// [Just 1, Just 2, Just 3, Nothing, Nothing]
typealias Pole = Maybe<(left:Int, right:Int)>
func leftLand(birds:Int)(pole:(Int, Int)) -> Pole {
if let (x, y) == pole {
if abs((x + birds) - y) < 4 {
return .Just((x+birds, y))
}
}
return .Nothing
}
/*:
모나드의 멋진점은 연속된 일련의 처리 중에 실패하는 경우(값이 없거나 유효하지 않게 되거나)에
이후의 흐름을 별도의 분기처리없이 함수를 연속적으로 체이닝하는 것으로 처리할 수 있다는 점이다.
이 문서의 이전 버전에서는 막대기를 들고 줄타기를 하는 중에 새가 막대 양쪽에 앉는 경우를 가정했었다.
이 부분을 커스텀 모나드로 적용해 보도록 하자.
** Swift의 커리드 함수는 반드시 인자명을 넣어야 해서 >>= 연산자의 적용을 할 수 없다. 따라서
클로저를 리턴하는 함수를 간접적으로 만들어야 한다.
*/
typealias Pole = Maybe<(Int, Int)>
func landLeft(n:Int) -> ((Int, Int)) -> Pole {
// ~~~~~~~~~~~~~~~~~~~~~
// raw한 값을 받아 모나드 값을 리턴하는 것이 포인트!
return {
(p:(Int, Int)) -> Pole in
let (x, y) = p
if x + n - y > 4 {
return .Nothing
}
let r = (x + n, y)
return .Just(r)
}
}
func landRight(n:Int) -> ((Int, Int)) -> Pole {
// ~~~~~~~~~~~~~~~~~~~~~
return {
(p:(Int, Int)) -> Pole in
let (x, y) = p
if y + n - x > 4 {
return .Nothing
}
let r = (x , y + n)
return .Just(r)
}
}
let pole:Pole = monad((0, 0))
let result = pole >>= landLeft(3) >>= landRight(5) >>= landRight(1) >>= landLeft(6)
println(result)
let result2 = pole >>= landLeft(3) >>= landRight(4) >>= landRight(5) >>= landLeft(7)
println(result2)
/*
Swift의 옵셔널타입도 모나드이다.
분명한 예로 옵셔널 체이닝이 있다.
someObject.somePropertyCanBeNil?.propertyMethod()
는 somePropertyCanBeNil이 nil 이 아니면 .propertyMethod()를 호출한 리턴값을
옵셔널로 돌려주고, 그렇지 않다면 nil을 돌려준다.
모나드를 이용한 이러한 체이닝의 장점은
모나드를 특정한 컨텍스트로 봤을 떄,
순수한 값을 컨텍스트로 감싼 후 바인딩을 연속적으로 해 나가면
그 횟수만큼 감싼 깊이가 반복되는 것이 아니라,
1차적인 컨텍스트만이 적용된다는 것이다.
"실패할 수 있는" 컨텍스트는 굳이 모나드가 아니더라도 감싸기만 하면
되는 것이기는 하다. 하지만 그것을 계산과정에서 일일이 언래핑하지 않는다면
그 결과가 계속 중첩될 수 있다는 것이다.
Optional<Optional<Optional<Optional<2>>>>
같은 값을 다시 언래핑하는 것은 여간 귀찮은 일이 아니지 않겠는가.
*/
struct Pole2: Printable {
var left: Int = 0
var right: Int = 0
func landLeft(n:Int) -> Pole2? {
if left + n - right > 4 {
return nil
}
return Pole2(left:left+n, right:right)
}
func landRight(n:Int) -> Pole2? {
if right + n - left > 4 {
return nil
}
return Pole2(left:left, right:right + n)
}
var description: String {
return "Pole(\(left), \(right))"
}
}
let ps = Pole2(left:0, right:0)
let result_success = ps.landLeft(3)?.landRight(2)?.landRight(4)?.landLeft(7)
let result_fail = ps.landLeft(3)?.landLeft(12)?.landRight(5).randRight(10)
// ^^^^^^^^^^^^^ 이시점에서 떨어져서 사망... nil이 된다.
println(result_success) // Optional(Pole(10, 6))
println(result_fail) // nil
@sooop
Copy link
Author

sooop commented Jul 22, 2015

여기서는 swift 언어의 한계 때문에 바인딩이 가능한 함수가 (T) -> T 시그니처만 가질 수 있다. 이와 동일한 문제가 Functor 구현쪽에도 있는데, 이는 Swift가 아직 higher-kinded 타입을 지원하지 못하기 때문이다. (함수형 언어라하더라도 이를 모두 지원하는 것은 아니다. 하스켈과 스칼라에서는 이것을 지원하지만 ML만 해도 이건 없는 기능임)

만약 이 기능이 지원된다면 프로토콜의 모양은 다음과 같아질 것이다. (Functor 역시 같은 이유로 단일 연관타입에 대해서만 만들 수 있으니)

protocoal Monad<T>:Functor {
    static func retun(a:T) -> Self<T>
    func bind<U>(f:(T) -> U) -> Self<U>
}

func >>= <T, U>(l:Monad<T>, r:(T) -> U) -> Monad<U> {
    return l.bind(r)
} 

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