본문 바로가기

프로그래밍 기술 노트/Functional Study

[Monad] 내멋대로 Monad 이해하기 - Bind (FlatMap), Monad

C#이나 JAVA 에서 보통 함수를 어떻게 만들까?

예를 들어서 number를 받아서 User를 만드는 함수라고 하고한다면...

public class User
{
    public int id { get; set; }
}
public User CreateUser (int id)
{
    return new User { id = id };
}

뭐 이런식으로 만들지 않을까?

그런데 여기서 User Id 가 0인 경우 관리자기 때문에 만들지 못한게 한다면?

public class User
{
    public int id { get; set; }
}
public User CreateUser (int id)
{
    if (id == 0)
    {
        return null;
    }
    return new User { id = id };
}

뭐 간단하게 이런식으로 처리할수있다. 아니면 Exception 을 내던가..

 

위 함수는 int -> User(== Nullable 함) 의 형태의 함수이다.

이를 똑같이 F#과 같은 FP 에서 Option 을 적용한다면

type User = {id :int}

let createUser id =
    if id = 0 then None else Some {id=id} 

이런식으로 만들지 않을까?

위 함수는 int 를 받아서 Option[User] 를 반환하는 함수이다.

별 문제는 없어보인다.

 

자 그럼 ID 를 입력받는 함수를 만들어보자

콘솔입력은 받는다고 할때, string 값을 받아오므로 이를 Int 로 파싱해서 성공하면 Some[int] 실패하면 None 을 반환하게 해보자.

let tryParse (str:string) =
    match Int32.TryParse str with
    | true, value -> Some value
    | _           -> None

Dot.Net 의 Int32.TryParse 를 이용하고 FP 스럽게 Option 으로 반환하는것이 끝이다.

역시 별 문제 없어 보인다.

 

자그럼 콘솔 입력을 받아서 int 로 변환하고 User를 만들어보자

자.. 당연히 오류가 난다. 왜냐?

tryParse 는 Option 을 리턴하는데 createUser는 int 를 받으니까..

그럼 어떻게 해야할까...?

 

어 그런데 이전 자료에서 map 을 배웠으니까 우리는 Option 내부값에 함수를 적용할수있다.

당장 해보자.

	let user = Console.ReadLine()
           |> tryParse
           |> Option.map createUser

크... 깔끔하다.

그리고 user의 타입은  Option[Option[User]] 이다 ....

당연한것이다. Map 은 Input과 Output의 타입을 모두 Lift 해버리니까 Option[User] 가 한번더 Option 으로 승급된것이다.

따라서 userValue의 값을 확인하라면 User.Value.Value 다. 크흠..

지금은 함수 2개를 사용했는데

만약 함수체인이 100개라면? User.Value.Value.Value.Value.Value.Value ... 이 우리가 원하던 값이다.

 

즉 지금의 문제는 tryParse 의 Output 이 createUser 의 Input 과 맞지 않는다는점에있다.

맞지 않으면 바로 사용할수없다, 맞지 않으면 함수를 합성할수없다.

그럼 어떻게 해야하지? 맞춰야된다.

let tryParse (str:string) =
    match Int32.TryParse str with
    | true, value -> Some value
    | _           -> None

let createUser (id:Option<int>) =
    if id.IsSome then 
        if id.Value = 0 then None else Some {id=id}
    else
        None

createUser의 Input 을 맞춰보았다.

딱봐도 개판이다. 일단 createUser는 Option 타입을 받아야된다. 생각지도 못하게 평범한 함수에서 뭔가 파라미터가 변경되었다.

그리고 if 문으로 결국 구분하는거면 이게 null 처리랑 뭐가 다른것일까? 오히려 더 복잡해졌다.

 

이러한 문제점을 해결하기 위한 기술이 "Bind" 이다

createUser를 복잡하게 바꿀것없이

Option.map 대신 Option.bind 를 쓰면 알아서 해결된다.

 

Bind 가 뭐길래? Bind 는 다른말로 FlatMap, SelectMany 이라고 한다. 어디서 많이 들어봣을텐데.. LINQ 든 뭐든 리스트 처리할떄 리스트안에있는 리스트를 (2차원 리스트) 를 1차원 리스트로 만들때 혹은 1차원 리스트처럼 처리하려고 할때 사용해봤을것이다.

대체 Bind 랑 2차원 리스트가 1차원 리스트가 되는거랑 무슨 상관이냐하면...

List 에서 Select (== Map)  과 SelectMany(== FlatMap) 의 동작은 위와같다. 넘겨준 함수는 자기자신을 리턴하는 함수이다.

 

List에서의 Select 와 SelectMany, Option 에서의 Map과 Bind의 동작을 살펴보면 위와같다.

List와 Option 이 동일한 구조를 가지는 것은 아니기때문에 완전 정확하지는 않지만

대충 결과를 보면 한곂씩 차이나는것을 확인할수있다.

 

즉 Bind 는 Option[int] 안에 있는 int 에 값을꺼내서 Option[?] 꼴의 함수를 적용하는것이다.

Map 과 Bind는 아래와 같다.

우리는 Option 에 적용했으므로 아래와 같이 표현할수있다.

 

 

이렇게 Bind 함수까지 해서 Map, Return, Apply, Bind 의 함수로

일반 세상의 함수, 값들을 Option 세상에 함수, 값들로 변환, 사용할수있으며, 효과적으로 함수를 합성할수있게 된다.

 

그래서 모나드는?

대충 값을 감쌀수있고 (int 를 Option int 로 감쌈)

map, return, apply, bind 의 함수를 가지고 있으면 (= 사용할수있으며)

 

대충 모나드

방금 사용한 Option 도 Maybe Monad 라고 불리는 일종의 모나드고

Map(=Select) , FlatMap (= SelectMany) 를 가지고 있는 List 도 List 모나드의 일종이다.

 

모나드면 모나드지 왜 모나드가 아니라 대충 모나드냐 하면...

저게 있다고 모나드는 아니기 때문이다, 뭔소리야 이게

모나드 자체가 카테고리론에서 온 말이기때문에..

A라는 것을 "모나드" 라고 부르기 위해서는 만족해야하는 규칙이 존재한다.

근데 이것을 설명하기에는 결합법칙.. 항등원... 뭐 어려운말도 많고 사실 필자도 잘 모르게때문에..

 

그러나 프로그래머로써 모나드를 사용하기 위해서는 대충 이정도만 알면 되기때문에

대충 모나드 이다. 필자가 주로 참고하는 fsharpforfunandprofit 에서도 모나드 굳이 어려운말로 쓰지말라고 한다.

 

 

모나드가 뭐가 좋은가?

우리는 이미 모나드라는것을 모르면서도 모나드를 쓰면서 그 장점을 몸소 느껴왔다.

 

C# 에서 지금까지 아주 잘 써왔던 LINQ는 리스트 모나드의 일종이다.

C#, Kotlin등에서 잘 사용하는 ? (옵셔널 컨디션) 으로 연결해서 호출하는 함수들

a?.b?.c?.d()

이러한 함수 호출은 MaybeMonad 나 TryMonad에서 연속적으로 Bind 하는것을 따라한것이다.

JS,C# 에서 마치 비동기를 동기처럼 다루게 해주는 마법의 키워드 async await 은 일종의 Async Monad의 구현체이다.

 

Java, C#, Kotlin 등 주류언어층에서도 이런한 모나드의 개념을 차용해서 새로운 문법을 추가하고있다.

Java, C#, Kotline, JS 등에서는 모나드 중에서 주로 사용하는 검증된 모나드들을 언어 피쳐로써 추가하는데 반하여

FP언어에서는 이러한 모나드를 원하면 "직접 구현" 해서 사용할수 있다.

 

 

참고자료

fsharpforfunandprofit.com/posts/elevated-world/

 

Understanding map and apply | F# for fun and profit

Part of the "Map and Bind and Apply, Oh my!" series (more) In this series of posts, I’ll attempt to describe some of the core functions for dealing with generic data types (such as Option and List). This is a follow-up post to my talk on functional patte

fsharpforfunandprofit.com

 

 

 

 

728x90