F# Options

F# Options (옵션)

F#에는 어떤 값에 실제 값이 없을 수도 있다는 상황을 표현하는 전용 타입이 있어요. 바로 option 타입이에요. 이름 붙은 값이나 변수에 값이 들었을 수도, 비어 있을 수도 있을 때 쓰죠. option 타입은 내부에 하나의 원본 타입을 담고, 그 타입의 값을 품거나 아무 값도 들지 않을 수 있어요.

출처: Options - F# | Microsoft Learn

본문

도입: 값이 "있을 수도, 없을 수도" 있을 때

개발을 하다 보면 "검색 결과가 없을 수도 있다", "파일을 여는데 실패할 수도 있다"처럼 값이 없다는 상태를 그대로 타입으로 표현하고 싶을 때가 있어요. F#에서는 이럴 때 option을 써요. 값이 있으면 Some(값), 없으면 None인 거죠. 어느 쪽인지가 타입에 담겨 있으니까, 비어 있는 값을 몰랐던 채로 써버리는 실수를 컴파일러가 미리 막아줘요.

Remarks (기본 개념)

option을 만들어 내는 함수 하나를 먼저 볼게요.

let keepIfPositive (a: int) =
    if a > 0 then Some(a)
    else None

여기서 입력 a가 0보다 크면 Some(a)가 만들어져요. 그렇지 않으면 None이 만들어지죠.

None은 옵션에 실제 값이 없을 때 쓰는 값이에요. 반대로 Some( ... )은 옵션에 값을 실어줘요. SomeNone은 패턴 매칭에서 특히 유용해요. 아래 exists 함수는 옵션에 값이 있으면 true를, 없으면 false를 돌려줘요.

let exists (x: int option) =
    match x with
    | Some(x) -> true
    | None -> false

Using Options (옵션 활용하기)

option검색 결과가 하나도 없을 수 있는 상황에서 흔히 쓰여요. 아래 코드가 그 전형이에요.

let rec tryFindMatch pred list =
    match list with
    | head :: tail -> if pred(head) then Some(head)
                      else tryFindMatch pred tail
    | [] -> None

// result1 is Some 100 and its type is int option.
let result1 = tryFindMatch (fun elem -> elem = 100) [ 200; 100; 50; 25 ]

// result2 is None and its type is int option.
let result2 = tryFindMatch (fun elem -> elem = 26) [ 200; 100; 50; 25 ]

위 코드는 리스트를 재귀적으로 훑어요. tryFindMatch는 술어 함수 pred(Boolean을 돌려주는 함수)와 검색할 리스트를 받죠. 조건을 만족하는 원소를 찾으면 재귀를 멈추고 Some(head)라는 옵션으로 값을 돌려줘요. 리스트를 끝까지 비웠다면 head를 찾지 못한 것이므로 None을 돌려주고요.

F# 라이브러리에는 컬렉션에서 값이 있을 수도 없을 수도 있는 원소를 찾는 함수가 많아요. 이런 함수들은 관례적으로 option을 돌려주고, 이름도 try 접두사로 시작해요. 예를 들면 Seq.tryFindIndex가 있어요.

값을 만들다 예외가 날 수도 있는 상황에서도 option이 유용해요. 아래 예시를 보죠.

open System.IO

let openFile filename =
    try
        let file = File.Open(filename, FileMode.Create)
        Some(file)
    with ex ->
        eprintf "An exception occurred with message %s" ex.Message
        None

위 예시의 openFile 함수는 string -> File option 타입이에요. 파일이 열리면 File 객체를, 예외가 나면 None을 돌려주니까요. 상황에 따라서는 예외를 잡아 삼키기보다 그냥 밖으로 흘려보내는 편이 더 나은 설계일 수도 있어요. 그건 상황에 맞게 판단하면 돼요.

참고로 Somenull이나 null 값이 들어갈 수도 있어요. 일반적인 F# 코드라면 보통 피하지만, .NET의 참조 타입 특성상 가능하긴 해요. 이건 지양하는 게 좋아요.

Option Properties and Methods (옵션의 속성과 메서드)

option 타입은 다음 속성과 메서드를 지원해요.

Property or method Type Description
None 'T option None 값을 가진 옵션 값을 만드는 정적 멤버예요.
IsNone bool 옵션이 None 값을 가지면 true를 돌려줘요.
IsSome bool 옵션이 None이 아닌 값을 가지면 true를 돌려줘요.
Some 'T option None이 아닌 값을 가진 옵션을 만드는 정적 멤버예요.
Value 'T 원본 값을 돌려줘요. 값이 None이면 System.NullReferenceException을 던져요.

Option Module (Option 모듈)

옵션을 다루는 유용한 함수를 모아 둔 Option 모듈도 있어요. 이 중 일부는 위 속성의 기능을 반복하지만, 함수가 필요한 맥락에서는 모듈 함수가 더 쓰기 편해요. Option.isSomeOption.isNone은 옵션이 값을 들고 있는지 검사하는 모듈 함수예요. Option.get은 값이 있으면 그 값을 얻어요. 값이 없으면 System.ArgumentException을 던지고요.

Option.bind 함수는 값이 있을 때 그 값에 함수를 적용해요. 이 함수는 인자를 정확히 하나 받아야 하고, 그 인자의 타입은 옵션 타입이어야 해요. 그리고 함수의 반환값도 다시 옵션 타입이에요.

Option 모듈에는 리스트, 배열, 시퀀스 같은 다른 컬렉션에서 쓰는 함수에 대응하는 함수들도 있어요. Option.map, Option.iter, Option.forall, Option.exists, Option.foldBack, Option.fold, Option.count 같은 함수들이죠. 덕분에 옵션을 원소가 0개 또는 1개인 컬렉션처럼 다룰 수 있어요. 자세한 내용과 예시는 컬렉션 함수 이야기(Lists)에서 다룰게요.

Converting to Other Types (다른 타입으로 변환하기)

option은 리스트나 배열로 변환할 수 있어요. 어느 쪽으로 바꾸든 결과는 원소가 0개 또는 1개인 구조예요. 배열로 바꾸려면 Option.toArray, 리스트로 바꾸려면 Option.toList를 쓰면 돼요.

Converting Options with Default Values (기본값을 써서 변환하기)

리스트·배열로의 변환 외에도, Option.defaultValue 함수로 기본값을 제공해 다른 타입으로 변환할 수 있어요. 값이 None이 되지 않도록 보장하고 싶을 때 특히 유용하죠. 예를 들어볼게요.

let optionString = Some("F#")
let defaultString = optionString |> Option.defaultValue ""
// defaultString is "F#"

let optionInt = None
let defaultInt = optionInt |> Option.defaultValue 0
// defaultInt is 0

Option.defaultValue 함수 덕분에 패턴 매칭을 쓰지 않고도 SomeNone 두 경우를 자연스럽게 처리할 수 있어요.

더 알아보기