Last active
February 3, 2020 16:20
-
-
Save sooop/0206a746dbfeb6a54074 to your computer and use it in GitHub Desktop.
Monad in Swift : 모나드 개념을 Swift로 구현해본다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
모나드는 특정한 타입을 감싸는 타입이며, | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
여기서는 swift 언어의 한계 때문에 바인딩이 가능한 함수가 (T) -> T 시그니처만 가질 수 있다. 이와 동일한 문제가 Functor 구현쪽에도 있는데, 이는 Swift가 아직 higher-kinded 타입을 지원하지 못하기 때문이다. (함수형 언어라하더라도 이를 모두 지원하는 것은 아니다. 하스켈과 스칼라에서는 이것을 지원하지만 ML만 해도 이건 없는 기능임)
만약 이 기능이 지원된다면 프로토콜의 모양은 다음과 같아질 것이다. (Functor 역시 같은 이유로 단일 연관타입에 대해서만 만들 수 있으니)