F# 4.5의 새로운 기능

F# 4.5의 새로운 기능 (What's new in F# 4.5)

F# 4.5에는 언어를 개선한 변경점이 여럿 들어 있어요. 이 기능들은 대부분 한 가지 목표를 위해 함께 추가되었는데, 바로 F#으로 더 효율적인 코드를 작성하면서도 그 코드가 안전하다는 점을 보장하자는 거예요. 그런데 이렇게 하려면 언어에 몇 가지 개념을 더하고, 이 구문들을 쓸 때 컴파일러가 꽤 많은 분석을 수행해야 하죠.

출처: What's new in F# 4.5 | Microsoft Learn

시작하기 (Get started)

F# 4.5는 모든 .NET Core 배포판과 Visual Studio 도구에서 쓸 수 있어요. 아직 시작 전이시라면 F# 시작하기 문서를 먼저 보시는 걸 추천해요.

Span과 byref 유사 구조체 (Span and byref-like structs)

.NET Core에서 도입된 Span<T> 타입은 메모리 안의 버퍼를 강력한 타입으로 표현하게 해 주는 건데요, F# 4.5부터는 F#에서도 이걸 사용할 수 있게 됐어요. 다음 예시는 Span<T>를 인자로 받는 함수 하나를 서로 다른 버퍼 표현에 그대로 재사용하는 모습을 보여 줘요.

let safeSum (bytes: Span<byte>) =
    let mutable sum = 0
    for i in 0 .. bytes.Length - 1 do
        sum <- sum + int bytes[i]
    sum

// managed memory
let arrayMemory = Array.zeroCreate<byte>(100)
let arraySpan = new Span<byte>(arrayMemory)

safeSum(arraySpan) |> printfn "res = %d"

// native memory
let nativeMemory = Marshal.AllocHGlobal(100);
let nativeSpan = new Span<byte>(nativeMemory.ToPointer(), 100)

safeSum(nativeSpan) |> printfn "res = %d"
Marshal.FreeHGlobal(nativeMemory)

// stack memory
let mem = NativePtr.stackalloc<byte>(100)
let mem2 = mem |> NativePtr.toVoidPtr
let stackSpan = Span<byte>(mem2, 100)

safeSum(stackSpan) |> printfn "res = %d"

여기서 짚고 넘어가야 할 중요한 점이 있어요. Span과 그 밖의 byref 유사 구조체(byref-like structs)는 컴파일러가 아주 엄격한 정적 분석을 수행해서, 예상 밖이라 느껴질 만큼 그 사용이 제한돼요. 이것이 F# 4.5에서 도입된 성능·표현력·안전성 사이의 근본적인 트레이드오프죠.

새로워진 byref (Revamped byrefs)

F# 4.5 이전에는 F#의 byref가 여러 응용에서 안전하지 않고 sound하지 않기도 했어요. F# 4.5에서 byref의 soundness 문제가 해결됐고, span과 byref 유사 구조체에 적용하던 것과 똑같은 정적 분석이 여기에도 적용됐어요.

inref<'T>와 outref<'T>

읽기 전용(read-only)·쓰기 전용(write-only)·읽기/쓰기(read/write) 관리 포인터를 각각 표현하기 위해, F# 4.5는 inref<'T>와 outref<'T> 타입을 새로 도입했어요. 각각 읽기 전용·쓰기 전용 포인터를 나타내죠. 이 둘은 의미(semantics)가 서로 달라요. 예를 들어 inref<'T>에는 값을 쓸 수 없어요.

let f (dt: inref<DateTime>) =
    dt <- DateTime.Now // ERROR - cannot write to an inref!

기본적으로 타입 추론은 관리 포인터를 inref<'T>로 추론해요. F# 코드의 불변(immutable)이라는 성격에 맞추려는 거죠. 이미 mutable로 선언된 경우는 얘기가 달라요. 어떤 값을 쓰기 가능하게 만들고 싶다면, 그 값을 다루는 함수나 멤버에 주소를 넘기기 전에 그 타입을 mutable로 선언해 줘야 해요. 더 자세한 내용은 Byrefs 문서를 참고해 주세요.

읽기 전용 구조체 (Readonly structs)

F# 4.5부터는 다음과 같이 구조체에 IsReadOnlyAttribute를 달 수 있어요.

[<IsReadOnly; Struct>]
type S(count1: int, count2: int) =
    member x.Count1 = count1
    member x.Count2 = count2

이렇게 하면 구조체 안에 mutable 멤버를 선언할 수 없게 되고, 어셈블리에서 이 구조체를 가져다 쓸 때 F#과 C# 모두 readonly로 취급하도록 메타데이터가 기록돼요. 자세한 내용은 ReadOnly structs 문서를 확인해 주세요.

void 포인터 (Void pointers)

F# 4.5에는 voidptr 타입이 추가됐고, 다음 함수들도 함께 추가됐어요.

  • NativePtr.ofVoidPtr — void 포인터를 native int 포인터로 변환해요.
  • NativePtr.toVoidPtr — native int 포인터를 void 포인터로 변환해요.

void 포인터를 활용하는 네이티브 컴포넌트와 연동(interop)할 때 유용해요.

match! 키워드 (The match! keyword)

match! 키워드는 계산 식(computation expression) 안에서 패턴 매칭을 한층 강화해 줘요.

// Code that returns an asynchronous option
let checkBananaAsync (s: string) =
    async {
        if s = "banana" then
            return Some s
        else
            return None
    }

// Now you can use 'match!'
let funcWithString (s: string) =
    async {
        match! checkBananaAsync s with
        | Some bananaString -> printfn "It's banana!"
        | None -> printfn "%s" s
}

이 덕분에 option(이나 다른 타입)을 async 같은 계산 식과 섞을 때 생기던 코드를 더 간결하게 줄일 수 있어요. 더 자세한 내용은 match! 문서를 봐 주세요.

배열·리스트·시퀀스 식에서 완화된 업캐스팅 요구 (Relaxed upcasting requirements in array, list, and sequence expressions)

배열·리스트·시퀀스 식 안에서 서로 상속 관계에 있는 타입을 섞어 쓸 때는, 전통적으로 :>나 upcast로 파생 타입을 부모 타입까지 업캐스팅해야 했어요. 이제 이 요구가 완화됐어요. 다음 예시를 볼게요.

let x0 : obj list  = [ "a" ] // ok pre-F# 4.5
let x1 : obj list  = [ "a"; "b" ] // ok pre-F# 4.5
let x2 : obj list  = [ yield "a" :> obj ] // ok pre-F# 4.5

let x3 : obj list  = [ yield "a" ] // Now ok for F# 4.5, and can replace x2

배열·리스트 식의 들여쓰기 완화 (Indentation relaxation for array and list expressions)

F# 4.5 이전에는 메서드 호출의 인자로 배열·리스트 식을 넘길 때 과하게 들여써야 했어요. 이제는 그럴 필요가 없어요.

module NoExcessiveIndenting =
    System.Console.WriteLine(format="{0}", arg = [|
        "hello"
    |])
    System.Console.WriteLine([|
        "hello"
    |])

더 알아보기