F# 6의 새로운 기능
F# 6의 새로운 기능
F# 6은 F# 언어와 F# Interactive에 여러 가지 개선을 더한 버전이에요. .NET 6과 함께 출시됐고요. 여기서는 그중에서도 실무에서 바로 쓸 만한 변화들을 중심으로 하나씩 살펴볼게요.
출처
- 원문: What's new in F# 6 — Microsoft Learn
- F# 6는 .NET 6와 함께 릴리스되었어요. 최신 .NET SDK는 .NET downloads page에서 받을 수 있어요.
본문
시작하기 (Get started)
F# 6은 모든 .NET Core 배포판과 Visual Studio 도구에서 쓸 수 있어요. 더 자세한 내용은 Get started with F# 문서를 참고하면 돼요.
task {…} 표현식
F# 6에는 task 표현식으로 F# 코드에서 .NET Task를 직접 만들 수 있는 네이티브 지원이 추가됐어요. task 표현식은 async 표현식과 비슷한데, .NET Task를 직접 다룰 수 있다는 점이 달라요.
예를 들어, 아래는 .NET 호환이 가능한 Task를 만드는 기존 F# 코드예요.
let readFilesTask (path1, path2) =
async {
let! bytes1 = File.ReadAllBytesAsync(path1) |> Async.AwaitTask
let! bytes2 = File.ReadAllBytesAsync(path2) |> Async.AwaitTask
return Array.append bytes1 bytes2
} |> Async.StartAsTask
F# 6에서는 이 코드를 다음과 같이 다시 쓸 수 있어요.
let readFilesTask (path1, path2) =
task {
let! bytes1 = File.ReadAllBytesAsync(path1)
let! bytes2 = File.ReadAllBytesAsync(path2)
return Array.append bytes1 bytes2
}
사실 F# 5에서도 훌륭한 TaskBuilder.fs나 Ply 같은 라이브러리로 Task 지원을 쓸 수 있었어요. 그래서 기존 코드를 내장 지원으로 옮기는 건 어렵지 않아요. 다만 차이가 좀 있는데요. 내장 지원과 이 라이브러리들 사이에 네임스페이스와 타입 추론이 조금씩 달라서 추가 타입 표기(annotation)가 필요할 수 있어요. 꼭 필요하다면, 이 커뮤니티 라이브러리들을 명시적으로 참조하고 파일마다 올바른 네임스페이스를 open해서 F# 6에서 계속 써도 돼요.
task {…}를 쓰는 건 async {…}를 쓰는 것과 아주 비슷해요. 그리고 async {…}보다 몇 가지 장점이 있어요.
task {...}의 오버헤드가 더 낮아서, 비동기 작업이 빠르게 끝나는 hot code path에서는 성능이 좋아질 수 있어요.task {…}의 디버깅 스테핑과 스택 트레이스가 더 좋아요.- Task를 기대하거나 만들어내는 .NET 패키지와 상호 운용(interop)하기가 더 쉬워요.
만약 async {…}에 익숙하다면, 알아둬야 할 차이점도 있어요.
task {…}는 첫 번째 await 지점까지 즉시 실행돼요.task {…}는 취소 토큰(cancellation token)을 암묵적으로 전파하지 않아요.task {…}는 암묵적인 취소 검사를 수행하지 않아요.task {…}는 비동기 꼬리 호출(asynchronous tailcall)을 지원하지 않아요. 즉, 중간에 비동기 대기가 없다면return! ..를 재귀적으로 쓰다가 스택 오버플로가 날 수 있어요.
전반적으로, Task를 쓰는 .NET 라이브러리와 상호 운용하고 있고 비동기 꼬리 호출이나 암묵적 취소 토큰 전파에 의존하지 않는다면, 새 코드에서는 async {…}보다 task {…}를 쓰는 걸 고려해 보세요. 기존 코드에서는 앞서 언급한 async {…}의 특징들에 의존하지 않는지 코드를 검토한 뒤에 task {…}로 바꾸는 게 좋아요.
이 기능은 F# RFC FS-1097을 구현한 거예요.
expr[idx] 간단 인덱싱 문법
F# 6에서는 컬렉션을 인덱싱하거나 슬라이싱할 때 expr[idx] 문법을 쓸 수 있어요.
F# 5까지는 F#이 expr.[idx]를 인덱싱 문법으로 써 왔어요. expr[idx]를 허용하게 된 건, F#을 배우거나 처음 보는 사람들이 점 표기 인덱싱이 업계 표준 관행과 불필요하게 다르다고 느낀다는 반복적인 피드백에 기반한 거예요.
이건 파괴적 변경(breaking change)은 아니에요. 기본적으로 expr.[idx]를 써도 경고가 나지 않거든요. 다만 코드 정리를 제안하는 정보성 메시지(informational message)는 나와요. 추가 정보성 메시지도 선택적으로 켤 수 있어요. 예를 들어 expr.[idx] 표기의 사용을 보고하는 선택적 정보성 경고(/warnon:3366)를 활성화할 수 있어요. 자세한 내용은 Indexer Notation을 참고하세요.
새 코드에서는 인덱싱 문법으로 expr[idx]를 체계적으로 쓸 것을 권장해요.
이 기능은 F# RFC FS-1110을 구현한 거예요.
부분 활성 패턴의 struct 표현
F# 6은 "active patterns" 기능에 부분 활성 패턴(partial active pattern)을 위한 선택적 struct 표현을 더해줘요. 이렇게 하면 특성(attribute)을 써서 부분 활성 패턴이 value option을 반환하도록 제한할 수 있어요.
[<return: Struct>]
let (|Int|_|) str =
match System.Int32.TryParse(str) with
| true, int -> ValueSome(int)
| _ -> ValueNone
이 특성을 쓰는 건 필수예요. 사용하는 지점에서는 코드가 변하지 않아요. 결과적으로 할당(allocations)이 줄어들어요.
이 기능은 F# RFC FS-1039을 구현한 거예요.
계산 표현식에서 오버로드된 사용자 지정 연산
F# 6에서는 오버로드된 메서드에 CustomOperationAttribute를 쓸 수 있어요.
계산 표현식 빌더 content의 다음 용법을 생각해 볼게요.
let mem = new System.IO.MemoryStream("Stream"B)
let content = ContentBuilder()
let ceResult =
content {
body "Name"
body (ArraySegment<_>("Email"B, 0, 5))
body "Password"B 2 4
body "BYTES"B
body mem
body "Description" "of" "content"
}
여기서 body 사용자 지정 연산은 서로 다른 타입의 가변 개수 인자를 받아요. 이는 오버로딩을 사용하는 다음 빌더 구현으로 지원돼요.
type Content = ArraySegment<byte> list
type ContentBuilder() =
member _.Run(c: Content) =
let crlf = "\r\n"B
[|for part in List.rev c do
yield! part.Array[part.Offset..(part.Count+part.Offset-1)]
yield! crlf |]
member _.Yield(_) = []
[<CustomOperation("body")>]
member _.Body(c: Content, segment: ArraySegment<byte>) =
segment::c
[<CustomOperation("body")>]
member _.Body(c: Content, bytes: byte[]) =
ArraySegment<byte>(bytes, 0, bytes.Length)::c
[<CustomOperation("body")>]
member _.Body(c: Content, bytes: byte[], offset, count) =
ArraySegment<byte>(bytes, offset, count)::c
[<CustomOperation("body")>]
member _.Body(c: Content, content: System.IO.Stream) =
let mem = new System.IO.MemoryStream()
content.CopyTo(mem)
let bytes = mem.ToArray()
ArraySegment<byte>(bytes, 0, bytes.Length)::c
[<CustomOperation("body")>]
member _.Body(c: Content, [<ParamArray>] contents: string[]) =
List.rev [for c in contents -> let b = Text.Encoding.ASCII.GetBytes c in ArraySegment<_>(b,0,b.Length)] @ c
이 기능은 F# RFC FS-1056을 구현한 거예요.
as 패턴
F# 6에서는 as 패턴의 오른쪽이 다시 패턴이 될 수 있어요. 이건 타입 테스트가 입력에 더 강한 타입을 부여했을 때 중요해요. 다음 코드를 예로 볼게요.
type Pair = Pair of int * int
let analyzeObject (input: obj) =
match input with
| :? (int * int) as (x, y) -> printfn $"A tuple: {x}, {y}"
| :? Pair as Pair (x, y) -> printfn $"A DU: {x}, {y}"
| _ -> printfn "Nope"
let input = box (1, 2)
각 패턴 케이스에서 입력 객체는 타입 테스트를 거쳐요. as 패턴의 오른쪽은 이제 더 강한 타입으로 객체를 매칭할 수 있는 또 다른 패턴이 될 수 있어요.
이 기능은 F# RFC FS-1105을 구현한 거예요.
들여쓰기 문법 개정
F# 6은 들여쓰기 인지 문법(indentation-aware syntax) 사용의 여러 불일치와 제약을 제거했어요. RFC FS-1108을 참고하세요. 이렇게 해서 F# 4.0 이후로 F# 사용자들이 지적해 온 중요한 이슈 10개가 해결됐어요.
예를 들어 F# 5에서는 다음 코드는 허용됐어요.
let c = (
printfn "aaaa"
printfn "bbbb"
)
그런데 다음 코드는 허용되지 않았어요. (경고가 나왔죠.)
let c = [
1
2
]
F# 6에서는 둘 다 허용돼요. 이로써 F#이 더 단순하고 배우기 쉬워졌어요. F# 커뮤니티 기여자인 Hadrian Tang이 이 기능을 이끌었고, 특히 매우 가치 있는 체계적인 테스트 작업을 해줬어요.
이 기능은 F# RFC FS-1108을 구현한 거예요.
추가 암시적 변환
F# 6에서는 RFC FS-1093에 설명된 대로 추가 "implicit(암시적)" 및 "type-directed(타입 지시)" 변환을 활성화했어요.
이 변경은 세 가지 장점을 가져와요.
- 필요한 명시적 업캐스트(upcast)가 줄어들어요
- 필요한 명시적 정수 변환이 줄어들어요
- .NET 스타일 암시적 변환에 대한 일급(first-class) 지원이 추가돼요
이 기능은 F# RFC FS-1093을 구현한 거예요.
추가 암시적 업캐스트 변환
F# 6은 추가 암시적 업캐스트 변환을 구현해요. 예를 들어 F# 5 이하에서는 타입 표기가 있어도, 함수를 구현할 때 분기별로 표현식의 하위 타입이 달라지면 반환 표현식에 업캐스트가 필요했어요. 다음 F# 5 코드를 볼게요.
open System
open System.IO
let findInputSource () : TextReader =
if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
// On Monday a TextReader
Console.In
else
// On other days a StreamReader
File.OpenText("path.txt") :> TextReader
여기서 조건문의 분기는 각각 TextReader와 StreamReader를 계산하고, 두 분기의 타입을 맞추기 위해 업캐스트를 추가했어요. F# 6에서는 이 업캐스트가 자동으로 추가돼요. 그래서 코드가 더 간단해져요.
let findInputSource () : TextReader =
if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
// On Monday a TextReader
Console.In
else
// On other days a StreamReader
File.OpenText("path.txt")
추가 암시적 업캐스트가 사용되는 지점마다 경고를 표시하려면 /warnon:3388 경고를 선택적으로 켤 수 있어요. Optional warnings for implicit conversions에서 설명한 대로요.
암시적 정수 변환
F# 6에서는 두 타입이 모두 알려져 있을 때 32비트 정수를 64비트 정수로 넓혀요(widen). 전형적인 API 모양을 예로 볼게요.
type Tensor(…) =
static member Create(sizes: seq<int64>) = Tensor(…)
F# 5에서는 int64용 정수 리터럴을 써야 했어요.
Tensor.Create([100L; 10L; 10L])
또는
Tensor.Create([int64 100; int64 10; int64 10])
F# 6에서는 타입 추론 중에 소스와 대상 타입이 모두 알려져 있을 때, int32→int64, int32→nativeint, int32→double로의 넓히기가 자동으로 일어나요. 그래서 앞선 예처럼 int32 리터럴을 그대로 쓸 수 있어요.
Tensor.Create([100; 10; 10])
이 변경에도 불구하고 F#은 대부분의 경우 숫자 타입의 명시적 넓히기를 계속 사용해요. 예를 들어 암시적 넓히기는 int8이나 int16 같은 다른 숫자 타입, 또는 float32→float64에는 적용되지 않고, 소스나 대상 타입 중 하나가 알려지지 않은 경우에도 적용되지 않아요. 암시적 숫자 넓히기가 사용되는 지점마다 경고를 표시하려면 /warnon:3389 경고를 선택적으로 켤 수 있어요. Optional warnings for implicit conversions에서 설명한 대로요.
.NET 스타일 암시적 변환의 일급 지원
F# 6에서는 메서드를 호출할 때 .NET "op_Implicit" 변환이 F# 코드에서 자동으로 적용돼요. 예를 들어 F# 5에서는 XML용 .NET API를 다룰 때 XName.op_Implicit을 써야 했어요.
open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants(XName.op_Implicit "Item")
F# 6에서는 소스 표현식과 대상 타입에 대한 타입이 모두 있을 때, 인자 표현식에 op_Implicit 변환이 자동으로 적용돼요.
open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants("Item")
메서드 인자에서 op_Implicit 변환 확장이 사용되는 지점마다 경고를 표시하려면 /warnon:3395 경고를 선택적으로 켤 수 있어요. Optional warnings for implicit conversions에서 설명한 대로요.
참고 F# 6의 첫 릴리스에서는 이 경고 번호가
/warnon:3390이었어요. 충돌 때문에 나중에/warnon:3395로 바뀌었어요.
암시적 변환의 선택적 경고
타입 지시 변환과 암시적 변환은 타입 추론과 잘 맞지 않아서 이해하기 어려운 코드로 이어질 수 있어요. 그래서 이 기능이 F# 코드에서 남용되지 않도록 돕는 몇 가지 완화 장치가 있어요. 첫째, 소스와 대상 타입 모두가 모호함이나 추가 타입 추론 없이 강하게 알려져 있어야 해요. 둘째, 암시적 변환 사용을 보고하는 opt-in 경고를 활성화할 수 있는데, 그중 하나는 기본적으로 켜져 있어요.
/warnon:3388(추가 암시적 업캐스트)/warnon:3389(암시적 숫자 넓히기)/warnon:3391(메서드 인자가 아닌 곳의op_Implicit— 기본 켜짐)/warnon:3395(메서드 인자의op_Implicit)
팀에서 암시적 변환 사용을 모두 금지하고 싶다면, /warnaserror:3388, /warnaserror:3389, /warnaserror:3391, /warnaserror:3395를 지정할 수도 있어요.
이진 숫자 포맷
F# 6은 이진 숫자 포맷에 사용 가능한 포맷 지정자에 %B 패턴을 추가했어요. 다음 F# 코드를 볼게요.
printf "%o" 123
printf "%B" 123
이 코드는 다음 출력을 냅니다.
173
1111011
이 기능은 F# RFC FS-1100을 구현한 거예요.
use 바인딩에서의 discard
F# 6에서는 use 바인딩에서 _를 쓸 수 있어요. 예를 들어:
let doSomething () =
use _ = System.IO.File.OpenText("input.txt")
printfn "reading the file"
이 기능은 F# RFC FS-1102을 구현한 거예요.
InlineIfLambda
F# 컴파일러에는 코드를 인라인하는 최적화기가 있어요. F# 6에서는 코드가 선택적으로 나타낼 수 있는 새로운 선언적 기능을 추가했는데, 만약 어떤 인자가 람다 함수로 판정되면 그 인자 자체가 호출 지점에서 항상 인라인되도록 하는 거예요.
예를 들어 배열을 순회하는 다음 iterateTwice 함수를 볼게요.
let inline iterateTwice ([<InlineIfLambda>] action) (array: 'T[]) =
for j = 0 to array.Length-1 do
action array[j]
for j = 0 to array.Length-1 do
action array[j]
호출 지점이 이렇다면:
let arr = [| 1.. 100 |]
let mutable sum = 0
arr |> iterateTwice (fun x ->
sum <- sum + x)
인라인과 다른 최적화를 거친 뒤에는 이렇게 돼요.
let arr = [| 1.. 100 |]
let mutable sum = 0
for j = 0 to arr.Length-1 do
sum <- sum + arr[j]
for j = 0 to arr.Length-1 do
sum <- sum + arr[j]
이전 버전의 F#과 달리 이 최적화는 관련된 람다 표현식의 크기에 관계없이 적용돼요. 이 기능은 루프 언롤링(loop unrolling)과 비슷한 변환을 더 안정적으로 구현하는 데도 쓸 수 있어요.
opt-in 경고(/warnon:3517, 기본 꺼짐)를 켜면 코드에서 InlineIfLambda 인자가 람다 표현식에 바인딩되지 않은 지점을 알려줘요. 보통은 이 경고를 켤 필요가 없어요. 다만 특정 종류의 고성능 프로그래밍에서는 모든 코드가 인라인되고 펼쳐지는지 확인하는 게 유용할 수 있어요.
이 기능은 F# RFC FS-1098을 구현한 거예요.
재개 가능한 코드 (Resumable code)
F# 6의 task {…} 지원은 RFC FS-1087이라는 재개 가능한 코드(resumable code)라는 기반 위에 세워졌어요. 재개 가능한 코드는 다양한 종류의 고성능 비동기 및 yield 상태 머신을 만드는 데 쓸 수 있는 기술적 기능이에요.
추가 컬렉션 함수
FSharp.Core 6.0.0은 핵심 컬렉션 함수에 다섯 가지 새 연산을 추가했어요. 이 함수들은 다음과 같아요.
List/Array/Seq.insertAtList/Array/Seq.removeAtList/Array/Seq.updateAtList/Array/Seq.insertManyAtList/Array/Seq.removeManyAt
이 함수들은 모두 해당 컬렉션 타입이나 시퀀스에 대해 복사-후-갱신(copy-and-update) 연산을 수행해요. 이런 연산은 일종의 "기능적 갱신(functional update)"이에요. 예시는 해당 문서를 참고하면 되는데, 예를 들면 List.insertAt이에요.
예로 Elmish 스타일로 작성한 간단한 "Todo List" 애플리케이션의 model, message, update 로직을 볼게요. 여기서 사용자가 애플리케이션과 상호작용하며 메시지를 만들고, update 함수가 이 메시지들을 처리해 새 model을 만들어내요.
type Model =
{ ToDo: string list }
type Message =
| InsertToDo of index: int * what: string
| RemoveToDo of index: int
| LoadedToDos of index: int * what: string list
let update (model: Model) (message: Message) =
match message with
| InsertToDo (index, what) ->
{ model with ToDo = model.ToDo |> List.insertAt index what }
| RemoveToDo index ->
{ model with ToDo = model.ToDo |> List.removeAt index }
| LoadedToDos (index, what) ->
{ model with ToDo = model.ToDo |> List.insertManyAt index what }
이 새 함수들 덕분에 로직이 명확하고 단순해지고, 불변 데이터만 사용하게 돼요.
이 기능은 F# RFC FS-1113을 구현한 거예요.
Map의 Keys와 Values
FSharp.Core 6.0.0에서 Map 타입은 이제 Keys와 Values 속성을 지원해요. 이 속성들은 기본 컬렉션을 복사하지 않아요.
이 기능은 F# RFC FS-1113에 문서화되어 있어요.
NativePtr 추가 intrinsics
FSharp.Core 6.0.0은 NativePtr 모듈에 새 intrinsics를 추가했어요.
NativePtr.nullPtrNativePtr.isNullPtrNativePtr.initBlockNativePtr.clearNativePtr.copyNativePtr.copyBlockNativePtr.ofILSigPtrNativePtr.toILSigPtr
NativePtr의 다른 함수들과 마찬가지로 이 함수들은 인라인되고, /nowarn:9를 쓰지 않으면 사용 시 경고가 나와요. 이 함수들의 사용은 비관리 타입(unmanaged types)으로 제한돼요.
이 기능은 F# RFC FS-1109에 문서화되어 있어요.
단위 표기가 추가된 숫자 타입
F# 6에서는 다음 타입이나 타입 약어(alias)들이 이제 단위 표기(unit-of-measure annotations)를 지원해요. 새로 추가된 항목은 굵게 표시했어요.
| F# 별칭 | CLR 타입 |
|---|---|
| float32/single | System.Single |
| float/double | System.Double |
| decimal | System.Decimal |
| sbyte/int8 | System.SByte |
| int16 | System.Int16 |
| int/int32 | System.Int32 |
| int64 | System.Int64 |
| byte/uint8 | System.Byte |
| uint16 | System.UInt16 |
| uint/uint32 | System.UInt32 |
| uint64 | System.UInt64 |
| nativeint | System.IntPtr |
| unativeint | System.UIntPtr |
예를 들어 부호 없는 정수에 단위를 붙일 수 있어요.
[<Measure>]
type days
let better_age = 3u<days>
이 기능은 F# RFC FS-1091에 문서화되어 있어요.
드물게 쓰는 기호 연산자에 대한 정보성 경고
F# 6은 F# 6 이후에서 :=, !, incr, decr의 사용을 탈락시키는(de-normalize) 부드러운 안내를 추가했어요. 이 연산자와 함수들을 쓰면 Value 속성을 명시적으로 쓰도록 코드를 바꾸라는 정보성 메시지가 나와요.
F# 프로그래밍에서 reference cell은 힙에 할당되는 가변 레지스터로 쓸 수 있어요. 가끔 유용하지만, 현대 F# 코딩에서는 let mutable을 대신 쓸 수 있기 때문에 거의 필요 없어요. F# 코어 라이브러리에는 reference cell과 관련된 두 연산자 :=, !와 두 함수 incr, decr가 있어요. 이 연산자들이 있으면 reference cell이 필요 이상으로 F# 프로그래밍의 중심이 돼서, 모든 F# 프로그래머가 이 연산자들을 알아야 해요. 게다가 ! 연산자는 C#이나 다른 언어의 not 연산과 쉽게 혼동될 수 있어서, 코드를 번역할 때 미묘한 버그의 원인이 될 수 있어요.
이 변경의 이유는 F# 프로그래머가 알아야 할 연산자 수를 줄여서 초보자에게 F#을 단순화하는 거예요.
예를 들어 다음 F# 5 코드를 볼게요.
let r = ref 0
let doSomething() =
printfn "doing something"
r := !r + 1
먼저, 현대 F# 코딩에서는 보통 let mutable을 대신 쓸 수 있기 때문에 reference cell이 거의 필요 없어요.
let mutable r = 0
let doSomething() =
printfn "doing something"
r <- r + 1
reference cell을 쓴다면 F# 6은 마지막 줄을 r.Value <- r.Value + 1로 바꾸라고 알리는 정보성 경고를 내보내고, reference cell의 적절한 사용에 대한 추가 안내 링크를 걸어줘요.
let r = ref 0
let doSomething() =
printfn "doing something"
r.Value <- r.Value + 1
이 메시지들은 경고가 아니라 IDE와 컴파일러 출력에 표시되는 "정보성 메시지(informational messages)"예요. F#은 여전히 이전 버전과 호환돼요.
이 기능은 F# RFC FS-1111을 구현한 거예요.
F# 도구: Visual Studio에서 스크립팅 기본값이 .NET 6
Visual Studio에서 F# Script(.fsx)를 열거나 실행하면, 기본적으로 스크립트가 64비트 실행으로 .NET 6를 사용해 분석되고 실행돼요. 이 기능은 Visual Studio 2019 후반 릴리스에서 미리 보기로 제공됐는데 이제 기본으로 켜졌어요.
.NET Framework 스크립팅을 활성화하려면 Tools > Options > F# Tools > F# Interactive로 가서 Use .NET Core Scripting을 false로 설정하고 F# Interactive 창을 다시 시작하면 돼요. 이 설정은 스크립트 편집과 실행 모두에 영향을 줘요. .NET Framework 스크립팅에서 32비트 실행을 활성화하려면 64-bit F# Interactive도 false로 설정하세요. .NET Core 스크립팅에는 32비트 옵션이 없어요.
F# 도구: F# 스크립트의 SDK 버전 고정
global.json 파일과 .NET SDK 설정이 들어 있는 디렉터리에서 dotnet fsi로 스크립트를 실행하면, 그 global.json에 적힌 버전의 .NET SDK가 스크립트를 실행하고 편집하는 데 사용돼요. 이 기능은 F# 5 후반 버전부터 사용할 수 있었어요.
예를 들어 다음 global.json 파일로 .NET SDK 버전 정책을 지정한 디렉터리에 스크립트가 있다고 가정해 볼게요.
{
"sdk": {
"version": "5.0.200",
"rollForward": "minor"
}
}
이제 이 디렉터리에서 dotnet fsi로 스크립트를 실행하면 SDK 버전이 존중돼요. 이건 스크립트를 컴파일·분석·실행하는 데 쓰는 SDK를 "잠글(lock down)" 수 있는 강력한 기능이에요.
Visual Studio나 다른 IDE에서 스크립트를 열고 편집하면, 도구가 스크립트를 분석·확인할 때 이 설정을 존중해요. SDK가 없다면 개발 머신에 설치해야 해요.
Linux나 다른 Unix 시스템에서는 shebang과 조합해서 스크립트를 직접 실행할 때 언어 버전도 지정할 수 있어요. script.fsx용 간단한 shebang은 다음과 같아요.
#!/usr/bin/env -S dotnet fsi
printfn "Hello, world"
이제 스크립트를 script.fsx로 직접 실행할 수 있어요. 특정 비기본 언어 버전과 이렇게 조합할 수도 있어요.
#!/usr/bin/env -S dotnet fsi --langversion:5.0
참고 이 설정은 편집 도구에서는 무시되는데, 편집 도구는 최신 언어 버전을 가정하고 스크립트를 분석하기 때문이에요.
레거시 기능 제거
F# 2.0 이후로 일부 폐기된 레거시 기능은 오랫동안 경고를 내보내 왔어요. F# 6에서 이 기능들을 쓰면 명시적으로 /langversion:5.0을 쓰지 않는 한 오류가 나요. 오류가 되는 기능들은 다음과 같아요.
- 접미 타입 이름을 쓰는 다중 제네릭 인자. 예:
(int, int) Dictionary. F# 6에서는 오류가 돼요. 표준 문법Dictionary<int,int>를 대신 써야 해요. #indent "off". 오류가 돼요.x.(expr). 오류가 돼요.module M = struct … end. 오류가 돼요.*.ml과*.mli입력 사용. 오류가 돼요.(*IF-CAML*)또는(*IF-OCAML*)사용. 오류가 돼요.land,lor,lxor,lsl,lsr,asr을 중위 연산자로 사용. 이 키워드들은 OCaml에서 중위 키워드였기 때문에 F#에서 중위 키워드이며 FSharp.Core에 정의되어 있지 않아요. 이 키워드를 쓰면 이제 경고가 나와요.
이건 F# RFC FS-1114를 구현한 거예요.
더 알아보기
- .NET downloads page
- Get started with F#
- task expressions
- Indexer Notation
- F# RFC FS-1097 — task builder
- F# RFC FS-1110 — index syntax
- F# RFC FS-1039 — struct representation for active patterns
- F# RFC FS-1056 — custom operation overloads
- F# RFC FS-1105 — as patterns
- F# RFC FS-1108 — indentation revisions
- F# RFC FS-1093 — additional conversions
- F# RFC FS-1100 — printf binary
- F# RFC FS-1102 — discards on use bindings
- F# RFC FS-1098 — InlineIfLambda
- F# RFC FS-1087 — resumable code
- F# RFC FS-1113 — insert/remove/update functions
- F# RFC FS-1109 — additional NativePtr intrinsics
- F# RFC FS-1091 — extend units of measure
- F# RFC FS-1111 — reference cell informational messages
- F# RFC FS-1114 — ml compatibility revisions