다음을 통해 공유


F# 개요 둘러보기

F#에 대해 알아보는 가장 좋은 방법은 F# 코드를 읽고 쓰는 것입니다. 이 문서는 F#의 주요 기능 중 일부를 둘러보는 역할을 하며 컴퓨터에서 실행할 수 있는 몇 가지 코드 조각을 제공합니다. 개발 환경 설정에 대해 알아보려면 시작확인하세요.

F#에는 함수와 형식이라는 두 가지 기본 개념이 있습니다. 이 투어는 이러한 두 가지 개념에 속하는 언어의 기능을 강조합니다.

온라인으로 코드 실행

컴퓨터에 F#이 설치되어 있지 않은 경우 Fable의 Try F#을 사용하여 브라우저에서 모든 샘플을 실행할 수 있습니다. Fable은 브라우저에서 직접 실행되는 F# 언어의 변형입니다. REPL에서 아래의 샘플을 보려면 Fable REPL의 왼쪽 메뉴 모음에서 샘플 > Learn > Tour of F#를 찾아보세요.

함수 및 모듈

모든 F# 프로그램의 가장 기본적인 요소는 모듈구성되지 함수입니다. Functions는 입력 작업을 수행하여 출력을 생성할 있으며, F#에서 항목을 그룹화하는 기본 방법인 모듈따라 구성됩니다. 함수에 이름을 지정하고 해당 인수를 정의하는 let 바인딩사용하여 정의됩니다.

module BasicFunctions =

    /// You use 'let' to define a function. This one accepts an integer argument and returns an integer.
    /// Parentheses are optional for function arguments, except for when you use an explicit type annotation.
    let sampleFunction1 x = x*x + 3

    /// Apply the function, naming the function return result using 'let'.
    /// The variable type is inferred from the function return type.
    let result1 = sampleFunction1 4573

    // This line uses '%d' to print the result as an integer. This is type-safe.
    // If 'result1' were not of type 'int', then the line would fail to compile.
    printfn $"The result of squaring the integer 4573 and adding 3 is %d{result1}"

    /// When needed, annotate the type of a parameter name using '(argument:type)'.  Parentheses are required.
    let sampleFunction2 (x:int) = 2*x*x - x/5 + 3

    let result2 = sampleFunction2 (7 + 4)
    printfn $"The result of applying the 2nd sample function to (7 + 4) is %d{result2}"

    /// Conditionals use if/then/elif/else.
    ///
    /// Note that F# uses white space indentation-aware syntax, similar to languages like Python.
    let sampleFunction3 x =
        if x < 100.0 then
            2.0*x*x - x/5.0 + 3.0
        else
            2.0*x*x + x/5.0 - 37.0

    let result3 = sampleFunction3 (6.5 + 4.5)

    // This line uses '%f' to print the result as a float.  As with '%d' above, this is type-safe.
    printfn $"The result of applying the 3rd sample function to (6.5 + 4.5) is %f{result3}"

let 바인딩은 다른 언어의 변수와 유사하게 이름에 값을 바인딩하는 방법입니다. let 바인딩은 기본적으로 변경할 수 없는 입니다. 이는 값이나 함수가 이름에 바인딩되면 원래 위치에서 변경할 수 없다는 것을 의미합니다. 이는 변경 가능한다른 언어의 변수와는 대조적입니다. 즉, 해당 값은 언제든지 변경할 수 있습니다. 변경 가능한 바인딩이 필요한 경우 let mutable ... 구문을 사용할 수 있습니다.

module Immutability =

    /// Binding a value to a name via 'let' makes it immutable.
    ///
    /// The second line of code compiles, but 'number' from that point onward will shadow the previous definition.
    /// There is no way to access the previous definition of 'number' due to shadowing.
    let number = 2
    // let number = 3

    /// A mutable binding.  This is required to be able to mutate the value of 'otherNumber'.
    let mutable otherNumber = 2

    printfn $"'otherNumber' is {otherNumber}"

    // When mutating a value, use '<-' to assign a new value.
    //
    // Note that '=' is not the same as this.  Outside binding values via 'let', '=' is used to test equality.
    otherNumber <- otherNumber + 1

    printfn $"'otherNumber' changed to be {otherNumber}"

숫자, 부울 및 문자열

.NET 언어인 F#은 .NET에 있는 것과 동일한 기본 기본 형식 지원합니다.

F#에서 다양한 숫자 형식을 나타내는 방법은 다음과 같습니다.

module IntegersAndNumbers =

    /// This is a sample integer.
    let sampleInteger = 176

    /// This is a sample floating point number.
    let sampleDouble = 4.1

    /// This computed a new number by some arithmetic.  Numeric types are converted using
    /// functions 'int', 'double' and so on.
    let sampleInteger2 = (sampleInteger/4 + 5 - 7) * 4 + int sampleDouble

    /// This is a list of the numbers from 0 to 99.
    let sampleNumbers = [ 0 .. 99 ]

    /// This is a list of all tuples containing all the numbers from 0 to 99 and their squares.
    let sampleTableOfSquares = [ for i in 0 .. 99 -> (i, i*i) ]

    // The next line prints a list that includes tuples, using an interpolated string.
    printfn $"The table of squares from 0 to 99 is:\n{sampleTableOfSquares}"

부울 값과 기본 조건부 논리 수행은 다음과 같습니다.

module Booleans =

    /// Booleans values are 'true' and 'false'.
    let boolean1 = true
    let boolean2 = false

    /// Operators on booleans are 'not', '&&' and '||'.
    let boolean3 = not boolean1 && (boolean2 || false)

    // This line uses '%b'to print a boolean value.  This is type-safe.
    printfn $"The expression 'not boolean1 && (boolean2 || false)' is %b{boolean3}"

기본 문자열 조작은 다음과 같습니다.

module StringManipulation =

    /// Strings use double quotes.
    let string1 = "Hello"
    let string2  = "world"

    /// Strings can also use @ to create a verbatim string literal.
    /// This will ignore escape characters such as '\', '\n', '\t', etc.
    let string3 = @"C:\Program Files\"

    /// String literals can also use triple-quotes.
    let string4 = """The computer said "hello world" when I told it to!"""

    /// String concatenation is normally done with the '+' operator.
    let helloWorld = string1 + " " + string2

    // This line uses '%s' to print a string value.  This is type-safe.
    printfn "%s" helloWorld

    /// Substrings use the indexer notation.  This line extracts the first 7 characters as a substring.
    /// Note that like many languages, Strings are zero-indexed in F#.
    let substring = helloWorld[0..6]
    printfn $"{substring}"

튜플

튜플은 F#에서 중요한 요소입니다. 값 자체로 처리할 수 있는 명명되지 않았지만 순서가 지정된 값의 그룹화입니다. 다른 값에서 집계되는 값으로 간주합니다. 함수에서 여러 값을 편리하게 반환하거나 일부 임시 편의를 위해 값을 그룹화하는 등 많은 용도를 사용합니다.

module Tuples =

    /// A simple tuple of integers.
    let tuple1 = (1, 2, 3)

    /// A function that swaps the order of two values in a tuple.
    ///
    /// F# Type Inference will automatically generalize the function to have a generic type,
    /// meaning that it will work with any type.
    let swapElems (a, b) = (b, a)

    printfn $"The result of swapping (1, 2) is {(swapElems (1,2))}"

    /// A tuple consisting of an integer, a string,
    /// and a double-precision floating point number.
    let tuple2 = (1, "fred", 3.1415)

    printfn $"tuple1: {tuple1}\ttuple2: {tuple2}"

struct 튜플을 만들 수도 있습니다. 또한 C#7/Visual Basic 15 튜플과 완전히 상호 운용되며, 이는 또한 struct 튜플입니다.

/// Tuples are normally objects, but they can also be represented as structs.
///
/// These interoperate completely with structs in C# and Visual Basic.NET; however,
/// struct tuples are not implicitly convertible with object tuples (often called reference tuples).
///
/// The second line below will fail to compile because of this.  Uncomment it to see what happens.
let sampleStructTuple = struct (1, 2)
//let thisWillNotCompile: (int*int) = struct (1, 2)

// Although you can
let convertFromStructTuple (struct(a, b)) = (a, b)
let convertToStructTuple (a, b) = struct(a, b)

printfn $"Struct Tuple: {sampleStructTuple}\nReference tuple made from the Struct Tuple: {(sampleStructTuple |> convertFromStructTuple)}"

struct 튜플은 값 형식이므로 암시적으로 참조 튜플로 변환하거나 그 반대로 변환할 수 없습니다. 참조 타입과 구조체 튜플 간에 명시적으로 변환해야 합니다.

파이프라인

파이프 연산자 |> F#에서 데이터를 처리할 때 광범위하게 사용됩니다. 이 연산자를 사용하면 유연한 방식으로 함수의 "파이프라인"을 설정할 수 있습니다. 다음 예제에서는 이러한 연산자를 활용하여 간단한 기능 파이프라인을 빌드하는 방법을 안내합니다.

module PipelinesAndComposition =

    /// Squares a value.
    let square x = x * x

    /// Adds 1 to a value.
    let addOne x = x + 1

    /// Tests if an integer value is odd via modulo.
    ///
    /// '<>' is a binary comparison operator that means "not equal to".
    let isOdd x = x % 2 <> 0

    /// A list of 5 numbers.  More on lists later.
    let numbers = [ 1; 2; 3; 4; 5 ]

    /// Given a list of integers, it filters out the even numbers,
    /// squares the resulting odds, and adds 1 to the squared odds.
    let squareOddValuesAndAddOne values =
        let odds = List.filter isOdd values
        let squares = List.map square odds
        let result = List.map addOne squares
        result

    printfn $"processing {numbers} through 'squareOddValuesAndAddOne' produces: {squareOddValuesAndAddOne numbers}"

    /// A shorter way to write 'squareOddValuesAndAddOne' is to nest each
    /// sub-result into the function calls themselves.
    ///
    /// This makes the function much shorter, but it's difficult to see the
    /// order in which the data is processed.
    let squareOddValuesAndAddOneNested values =
        List.map addOne (List.map square (List.filter isOdd values))

    printfn $"processing {numbers} through 'squareOddValuesAndAddOneNested' produces: {squareOddValuesAndAddOneNested numbers}"

    /// A preferred way to write 'squareOddValuesAndAddOne' is to use F# pipe operators.
    /// This allows you to avoid creating intermediate results, but is much more readable
    /// than nesting function calls like 'squareOddValuesAndAddOneNested'
    let squareOddValuesAndAddOnePipeline values =
        values
        |> List.filter isOdd
        |> List.map square
        |> List.map addOne

    printfn $"processing {numbers} through 'squareOddValuesAndAddOnePipeline' produces: {squareOddValuesAndAddOnePipeline numbers}"

    /// You can shorten 'squareOddValuesAndAddOnePipeline' by moving the second `List.map` call
    /// into the first, using a Lambda Function.
    ///
    /// Note that pipelines are also being used inside the lambda function.  F# pipe operators
    /// can be used for single values as well.  This makes them very powerful for processing data.
    let squareOddValuesAndAddOneShorterPipeline values =
        values
        |> List.filter isOdd
        |> List.map(fun x -> x |> square |> addOne)

    printfn $"processing {numbers} through 'squareOddValuesAndAddOneShorterPipeline' produces: {squareOddValuesAndAddOneShorterPipeline numbers}"

    /// Lastly, you can eliminate the need to explicitly take 'values' in as a parameter by using '>>'
    /// to compose the two core operations: filtering out even numbers, then squaring and adding one.
    /// Likewise, the 'fun x -> ...' bit of the lambda expression is also not needed, because 'x' is simply
    /// being defined in that scope so that it can be passed to a functional pipeline.  Thus, '>>' can be used
    /// there as well.
    ///
    /// The result of 'squareOddValuesAndAddOneComposition' is itself another function which takes a
    /// list of integers as its input.  If you execute 'squareOddValuesAndAddOneComposition' with a list
    /// of integers, you'll notice that it produces the same results as previous functions.
    ///
    /// This is using what is known as function composition.  This is possible because functions in F#
    /// use Partial Application and the input and output types of each data processing operation match
    /// the signatures of the functions we're using.
    let squareOddValuesAndAddOneComposition =
        List.filter isOdd >> List.map (square >> addOne)

    printfn $"processing {numbers} through 'squareOddValuesAndAddOneComposition' produces: {squareOddValuesAndAddOneComposition numbers}"

이전 샘플에서는 목록 처리 함수, 일류 함수 및 부분 애플리케이션포함하여 F#의 많은 기능을 사용했습니다. 고급 개념이지만 파이프라인을 빌드할 때 데이터를 처리하는 데 함수를 얼마나 쉽게 사용할 수 있는지는 분명해야 합니다.

목록, 배열 및 시퀀스

목록, 배열 및 시퀀스는 F# 코어 라이브러리의 세 가지 기본 컬렉션 형식입니다.

목록 동일한 형식의 요소의 순서가 변경 불가능한 컬렉션입니다. 그들은 단일 연결 리스트입니다. 즉, 열거를 위한 것이지만, 크기가 큰 경우 임의 액세스 및 연결에는 적합하지 않습니다. 이는 일반적으로 목록을 단일 연결 리스트로 표현하지 않는 다른 인기 있는 언어의 목록과는 대조적입니다.

module Lists =

    /// Lists are defined using [ ... ].  This is an empty list.
    let list1 = [ ]

    /// This is a list with 3 elements.  ';' is used to separate elements on the same line.
    let list2 = [ 1; 2; 3 ]

    /// You can also separate elements by placing them on their own lines.
    let list3 = [
        1
        2
        3
    ]

    /// This is a list of integers from 1 to 1000
    let numberList = [ 1 .. 1000 ]

    /// Lists can also be generated by computations. This is a list containing
    /// all the days of the year.
    ///
    /// 'yield' is used for on-demand evaluation. More on this later in Sequences.
    let daysList =
        [ for month in 1 .. 12 do
              for day in 1 .. System.DateTime.DaysInMonth(2017, month) do
                  yield System.DateTime(2017, month, day) ]

    // Print the first 5 elements of 'daysList' using 'List.take'.
    printfn $"The first 5 days of 2017 are: {daysList |> List.take 5}"

    /// Computations can include conditionals.  This is a list containing the tuples
    /// which are the coordinates of the black squares on a chess board.
    let blackSquares =
        [ for i in 0 .. 7 do
              for j in 0 .. 7 do
                  if (i+j) % 2 = 1 then
                      yield (i, j) ]

    /// Lists can be transformed using 'List.map' and other functional programming combinators.
    /// This definition produces a new list by squaring the numbers in numberList, using the pipeline
    /// operator to pass an argument to List.map.
    let squares =
        numberList
        |> List.map (fun x -> x*x)

    /// There are many other list combinations. The following computes the sum of the squares of the
    /// numbers divisible by 3.
    let sumOfSquares =
        numberList
        |> List.filter (fun x -> x % 3 = 0)
        |> List.sumBy (fun x -> x * x)

    printfn $"The sum of the squares of numbers up to 1000 that are divisible by 3 is: %d{sumOfSquares}"

배열은 동일한 형식의 요소로 구성된 변경 가능한 고정 크기의 컬렉션입니다. 요소의 빠른 임의 액세스를 지원하며, 인접한 메모리 블록이기 때문에 F# 목록보다 빠릅니다.

module Arrays =

    /// This is The empty array.  Note that the syntax is similar to that of Lists, but uses `[| ... |]` instead.
    let array1 = [| |]

    /// Arrays are specified using the same range of constructs as lists.
    let array2 = [| "hello"; "world"; "and"; "hello"; "world"; "again" |]

    /// This is an array of numbers from 1 to 1000.
    let array3 = [| 1 .. 1000 |]

    /// This is an array containing only the words "hello" and "world".
    let array4 =
        [| for word in array2 do
               if word.Contains("l") then
                   yield word |]

    /// This is an array initialized by index and containing the even numbers from 0 to 2000.
    let evenNumbers = Array.init 1001 (fun n -> n * 2)

    /// Sub-arrays are extracted using slicing notation.
    let evenNumbersSlice = evenNumbers[0..500]

    /// You can loop over arrays and lists using 'for' loops.
    for word in array4 do
        printfn $"word: {word}"

    // You can modify the contents of an array element by using the left arrow assignment operator.
    //
    // To learn more about this operator, see: https://learn.microsoft.com/dotnet/fsharp/language-reference/values/index#mutable-variables
    array2[1] <- "WORLD!"

    /// You can transform arrays using 'Array.map' and other functional programming operations.
    /// The following calculates the sum of the lengths of the words that start with 'h'.
    ///
    /// Note that in this case, similar to Lists, array2 is not mutated by Array.filter.
    let sumOfLengthsOfWords =
        array2
        |> Array.filter (fun x -> x.StartsWith "h")
        |> Array.sumBy (fun x -> x.Length)

    printfn $"The sum of the lengths of the words in Array 2 is: %d{sumOfLengthsOfWords}"

시퀀스 모두 동일한 형식의 논리적 요소 시리즈입니다. 이러한 형식은 목록 및 배열보다 더 일반적인 종류로서, 어떤 논리적 요소의 계열에 대한 "관점"이 될 수 있습니다. 또한 지연수 있기 때문에 두드러집니다. 즉, 필요한 경우에만 요소를 계산할 수 있습니다.

module Sequences =

    /// This is the empty sequence.
    let seq1 = Seq.empty

    /// This a sequence of values.
    let seq2 = seq { yield "hello"; yield "world"; yield "and"; yield "hello"; yield "world"; yield "again" }

    /// This is an on-demand sequence from 1 to 1000.
    let numbersSeq = seq { 1 .. 1000 }

    /// This is a sequence producing the words "hello" and "world"
    let seq3 =
        seq { for word in seq2 do
                  if word.Contains("l") then
                      yield word }

    /// This is a sequence producing the even numbers up to 2000.
    let evenNumbers = Seq.init 1001 (fun n -> n * 2)

    let rnd = System.Random()

    /// This is an infinite sequence which is a random walk.
    /// This example uses yield! to return each element of a subsequence.
    let rec randomWalk x =
        seq { yield x
              yield! randomWalk (x + rnd.NextDouble() - 0.5) }

    /// This example shows the first 100 elements of the random walk.
    let first100ValuesOfRandomWalk =
        randomWalk 5.0
        |> Seq.truncate 100
        |> Seq.toList

    printfn $"First 100 elements of a random walk: {first100ValuesOfRandomWalk}"

재귀 함수

요소의 컬렉션 또는 시퀀스 처리는 일반적으로 F#의 재귀 사용하여 수행됩니다. F#은 루프 및 명령적 프로그래밍을 지원하지만 정확성을 보장하는 것이 더 쉽기 때문에 재귀를 사용하는 것이 좋습니다.

메모

다음 예제에서는 match 식을 통해 패턴 일치를 사용합니다. 이 기본 구문은 이 문서의 뒷부분에서 다룹니다.

module RecursiveFunctions =

    /// This example shows a recursive function that computes the factorial of an
    /// integer. It uses 'let rec' to define a recursive function.
    let rec factorial n =
        if n = 0 then 1 else n * factorial (n-1)

    printfn $"Factorial of 6 is: %d{factorial 6}"

    /// Computes the greatest common factor of two integers.
    ///
    /// Since all of the recursive calls are tail calls,
    /// the compiler will turn the function into a loop,
    /// which improves performance and reduces memory consumption.
    let rec greatestCommonFactor a b =
        if a = 0 then b
        elif a < b then greatestCommonFactor a (b - a)
        else greatestCommonFactor (a - b) b

    printfn $"The Greatest Common Factor of 300 and 620 is %d{greatestCommonFactor 300 620}"

    /// This example computes the sum of a list of integers using recursion.
    ///
    /// '::' is used to split a list into the head and tail of the list,
    /// the head being the first element and the tail being the rest of the list.
    let rec sumList xs =
        match xs with
        | []    -> 0
        | y::ys -> y + sumList ys

    /// This makes 'sumList' tail recursive, using a helper function with a result accumulator.
    let rec private sumListTailRecHelper accumulator xs =
        match xs with
        | []    -> accumulator
        | y::ys -> sumListTailRecHelper (accumulator+y) ys

    /// This invokes the tail recursive helper function, providing '0' as a seed accumulator.
    /// An approach like this is common in F#.
    let sumListTailRecursive xs = sumListTailRecHelper 0 xs

    let oneThroughTen = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

    printfn $"The sum 1-10 is %d{sumListTailRecursive oneThroughTen}"

또한 F#은 루프 구문만큼 빠르게 재귀 호출을 최적화하는 방법인 Tail Call Optimization을 전폭적으로 지원합니다.

레코드 및 차별된 공용 구조체 형식

레코드 및 공용 구조체 형식은 F# 코드에 사용되는 두 가지 기본 데이터 형식이며 일반적으로 F# 프로그램에서 데이터를 나타내는 가장 좋은 방법입니다. 이렇게 하면 다른 언어의 클래스와 비슷하지만 주요 차이점 중 하나는 구조적 같음 의미 체계가 있다는 것입니다. 즉, "기본적으로" 서로 비교할 수 있으며, 같음을 확인하는 것은 간단합니다 - 하나가 다른 것과 같은지 확인하기만 하면 됩니다.

레코드는 명명된 값들의 집합으로, 선택적 멤버(예: 메서드)를 포함할 수 있습니다. C# 또는 Java에 익숙하다면, POCO 또는 POJO와 비슷하게 느껴질 것입니다. 단, 구조적 동등성과 더 간소화된 특징이 있습니다.

module RecordTypes =

    /// This example shows how to define a new record type.
    type ContactCard =
        { Name     : string
          Phone    : string
          Verified : bool }

    /// This example shows how to instantiate a record type.
    let contact1 =
        { Name = "Alf"
          Phone = "(206) 555-0157"
          Verified = false }

    /// You can also do this on the same line with ';' separators.
    let contactOnSameLine = { Name = "Alf"; Phone = "(206) 555-0157"; Verified = false }

    /// This example shows how to use "copy-and-update" on record values. It creates
    /// a new record value that is a copy of contact1, but has different values for
    /// the 'Phone' and 'Verified' fields.
    ///
    /// To learn more, see: https://learn.microsoft.com/dotnet/fsharp/language-reference/copy-and-update-record-expressions
    let contact2 =
        { contact1 with
            Phone = "(206) 555-0112"
            Verified = true }

    /// This example shows how to write a function that processes a record value.
    /// It converts a 'ContactCard' object to a string.
    let showContactCard (c: ContactCard) =
        c.Name + " Phone: " + c.Phone + (if not c.Verified then " (unverified)" else "")

    printfn $"Alf's Contact Card: {showContactCard contact1}"

    /// This is an example of a Record with a member.
    type ContactCardAlternate =
        { Name     : string
          Phone    : string
          Address  : string
          Verified : bool }

        /// Members can implement object-oriented members.
        member this.PrintedContactCard =
            this.Name + " Phone: " + this.Phone + (if not this.Verified then " (unverified)" else "") + this.Address

    let contactAlternate =
        { Name = "Alf"
          Phone = "(206) 555-0157"
          Verified = false
          Address = "111 Alf Street" }

    // Members are accessed via the '.' operator on an instantiated type.
    printfn $"Alf's alternate contact card is {contactAlternate.PrintedContactCard}"

레코드를 구조체로 나타낼 수도 있습니다. 이 작업은 [<Struct>] 특성으로 수행됩니다.

[<Struct>]
type ContactCardStruct =
    { Name     : string
      Phone    : string
      Verified : bool }

DU(구별된 공용체) 는 명명된 여러 양식 또는 사례가 될 수 있는 값입니다. 형식에 저장된 데이터는 여러 고유 값 중 하나일 수 있습니다.

module DiscriminatedUnions =

    /// The following represents the suit of a playing card.
    type Suit =
        | Hearts
        | Clubs
        | Diamonds
        | Spades

    /// A Discriminated Union can also be used to represent the rank of a playing card.
    type Rank =
        /// Represents the rank of cards 2 .. 10
        | Value of int
        | Ace
        | King
        | Queen
        | Jack

        /// Discriminated Unions can also implement object-oriented members.
        static member GetAllRanks() =
            [ yield Ace
              for i in 2 .. 10 do yield Value i
              yield Jack
              yield Queen
              yield King ]

    /// This is a record type that combines a Suit and a Rank.
    /// It's common to use both Records and Discriminated Unions when representing data.
    type Card = { Suit: Suit; Rank: Rank }

    /// This computes a list representing all the cards in the deck.
    let fullDeck =
        [ for suit in [ Hearts; Diamonds; Clubs; Spades] do
              for rank in Rank.GetAllRanks() do
                  yield { Suit=suit; Rank=rank } ]

    /// This example converts a 'Card' object to a string.
    let showPlayingCard (c: Card) =
        let rankString =
            match c.Rank with
            | Ace -> "Ace"
            | King -> "King"
            | Queen -> "Queen"
            | Jack -> "Jack"
            | Value n -> string n
        let suitString =
            match c.Suit with
            | Clubs -> "clubs"
            | Diamonds -> "diamonds"
            | Spades -> "spades"
            | Hearts -> "hearts"
        rankString  + " of " + suitString

    /// This example prints all the cards in a playing deck.
    let printAllCards() =
        for card in fullDeck do
            printfn $"{showPlayingCard card}"

DUS를 Single-Case 구분된 공용 구조체사용하여 기본 형식에 대한 도메인 모델링을 도울 수도 있습니다. 종종 문자열 및 기타 기본 형식은 무언가를 나타내는 데 사용되므로 특정 의미가 부여됩니다. 그러나 데이터의 기본 표현만 사용하면 실수로 잘못된 값을 할당할 수 있습니다. 각 유형의 정보를 고유한 단일 케이스 유니온으로 표시하면 이 시나리오에서 정확성을 보장할 수 있습니다.

// Single-case DUs are often used for domain modeling.  This can buy you extra type safety
// over primitive types such as strings and ints.
//
// Single-case DUs cannot be implicitly converted to or from the type they wrap.
// For example, a function which takes in an Address cannot accept a string as that input,
// or vice versa.
type Address = Address of string
type Name = Name of string
type SSN = SSN of int

// You can easily instantiate a single-case DU as follows.
let address = Address "111 Alf Way"
let name = Name "Alf"
let ssn = SSN 1234567890

/// When you need the value, you can unwrap the underlying value with a simple function.
let unwrapAddress (Address a) = a
let unwrapName (Name n) = n
let unwrapSSN (SSN s) = s

// Printing single-case DUs is simple with unwrapping functions.
printfn $"Address: {address |> unwrapAddress}, Name: {name |> unwrapName}, and SSN: {ssn |> unwrapSSN}"

위의 샘플에서 알 수 있듯이 단일 사례 구분 공용 구조체에서 기본 값을 얻으려면 명시적으로 래핑을 해제해야 합니다.

또한 DU는 재귀 정의를 지원하므로 트리와 본질적으로 재귀 데이터를 쉽게 나타낼 수 있습니다. 예를 들어 existsinsert 함수를 사용하여 이진 검색 트리를 나타내는 방법은 다음과 같습니다.

/// Discriminated Unions also support recursive definitions.
///
/// This represents a Binary Search Tree, with one case being the Empty tree,
/// and the other being a Node with a value and two subtrees.
///
/// Note 'T here is a type parameter, indicating that 'BST' is a generic type.
/// More on generics later.
type BST<'T> =
    | Empty
    | Node of value:'T * left: BST<'T> * right: BST<'T>

/// Check if an item exists in the binary search tree.
/// Searches recursively using Pattern Matching.  Returns true if it exists; otherwise, false.
let rec exists item bst =
    match bst with
    | Empty -> false
    | Node (x, left, right) ->
        if item = x then true
        elif item < x then (exists item left) // Check the left subtree.
        else (exists item right) // Check the right subtree.

/// Inserts an item in the Binary Search Tree.
/// Finds the place to insert recursively using Pattern Matching, then inserts a new node.
/// If the item is already present, it does not insert anything.
let rec insert item bst =
    match bst with
    | Empty -> Node(item, Empty, Empty)
    | Node(x, left, right) as node ->
        if item = x then node // No need to insert, it already exists; return the node.
        elif item < x then Node(x, insert item left, right) // Call into left subtree.
        else Node(x, left, insert item right) // Call into right subtree.

DU를 사용하면 데이터 형식에서 트리의 재귀 구조를 나타낼 수 있으므로 이 재귀 구조에서 작동하는 것은 간단하며 정확성을 보장합니다. 아래와 같이 패턴 일치에서도 지원됩니다.

패턴 매칭

패턴 매칭은 F# 형식에서의 정확성을 보장하는 F# 기능입니다. 위의 샘플에서는 꽤 많은 match x with ... 구문을 발견했을 것입니다. 이 구조체를 사용하면, 데이터 형식의 "셰이프"를 이해할 수 있는 컴파일러가 철저한 패턴 매칭이라는 방법을 통해 데이터 형식을 사용할 때 모든 가능한 경우를 처리하도록 강제할 수 있습니다. 이는 정확성을 위해 강력한 도구이며, 일반적으로 런타임 문제로 간주될 수 있는 것을 컴파일 시간 문제로 "리프트"하여 영리하게 해결할 수 있습니다.

module PatternMatching =

    /// A record for a person's first and last name
    type Person = {
        First : string
        Last  : string
    }

    /// A Discriminated Union of 3 different kinds of employees
    type Employee =
        | Engineer of engineer: Person
        | Manager of manager: Person * reports: List<Employee>
        | Executive of executive: Person * reports: List<Employee> * assistant: Employee

    /// Count everyone underneath the employee in the management hierarchy,
    /// including the employee. The matches bind names to the properties
    /// of the cases so that those names can be used inside the match branches.
    /// Note that the names used for binding do not need to be the same as the
    /// names given in the DU definition above.
    let rec countReports(emp : Employee) =
        1 + match emp with
            | Engineer(person) ->
                0
            | Manager(person, reports) ->
                reports |> List.sumBy countReports
            | Executive(person, reports, assistant) ->
                (reports |> List.sumBy countReports) + countReports assistant

다음 샘플에서는 이전에 알아차렸을 수 있는 _ 패턴을 소개합니다. 이를 와일드카드 패턴이라고 하며, 이는 "어떤 것이 무엇인지 신경 쓰지 않는다"고 말하는 방법입니다. 편리하지만, _사용하는 데 주의를 기울이지 않으면 실수로 철저한 패턴 일치를 우회할 수 있으며 컴파일 시간 적용의 이점을 더 이상 누릴 수 없습니다. 패턴 매칭 시 분해된 형식의 특정 부분을 신경 쓰지 않을 때나, 패턴 매칭 표현식에서 모든 의미 있는 사례를 열거했을 때 최종 절을 사용하는 것이 가장 바람직합니다.

다음 예제에서는 구문 분석 작업이 실패할 때 _ 사례가 사용됩니다.

/// Find all managers/executives named "Dave" who do not have any reports.
/// This uses the 'function' shorthand to as a lambda expression.
let findDaveWithOpenPosition(emps : List<Employee>) =
    emps
    |> List.filter(function
                   | Manager({First = "Dave"}, []) -> true // [] matches an empty list.
                   | Executive({First = "Dave"}, [], _) -> true
                   | _ -> false) // '_' is a wildcard pattern that matches anything.
                                 // This handles the "or else" case.

/// You can also use the shorthand function construct for pattern matching,
/// which is useful when you're writing functions which make use of Partial Application.
let private parseHelper (f: string -> bool * 'T) = f >> function
    | (true, item) -> Some item
    | (false, _) -> None

let parseDateTimeOffset = parseHelper DateTimeOffset.TryParse

let result = parseDateTimeOffset "1970-01-01"
match result with
| Some dto -> printfn "It parsed!"
| None -> printfn "It didn't parse!"

// Define some more functions which parse with the helper function.
let parseInt = parseHelper Int32.TryParse
let parseDouble = parseHelper Double.TryParse
let parseTimeSpan = parseHelper TimeSpan.TryParse

활성 패턴은 패턴 매칭에 사용할 수 있는 또 다른 강력한 구조입니다. 입력 데이터를 사용자 지정 양식으로 분할하여 패턴 일치 호출 사이트에서 분해할 수 있습니다. 또한 매개 변수화할 수 있으므로 파티션을 함수로 정의할 수 있습니다. 활성 패턴을 지원하도록 이전 예제를 확장하면 다음과 같습니다.

let (|Int|_|) = parseInt
let (|Double|_|) = parseDouble
let (|Date|_|) = parseDateTimeOffset
let (|TimeSpan|_|) = parseTimeSpan

/// Pattern Matching via 'function' keyword and Active Patterns often looks like this.
let printParseResult = function
    | Int x -> printfn $"%d{x}"
    | Double x -> printfn $"%f{x}"
    | Date d -> printfn $"%O{d}"
    | TimeSpan t -> printfn $"%O{t}"
    | _ -> printfn "Nothing was parse-able!"

// Call the printer with some different values to parse.
printParseResult "12"
printParseResult "12.045"
printParseResult "12/28/2016"
printParseResult "9:01PM"
printParseResult "banana!"

옵션

옵션 형식은 구분된 유니온 유형의 특별한 경우 중 하나로, 그 유용성이 매우 커서 F#의 플랫폼 독립형 라이브러리의 일부로 포함되어 있습니다.

옵션 형식는 두 가지 경우를 나타내는 형식으로, 값이 있거나 아무것도 없는 형태입니다. 값이 특정 작업으로 인해 발생하거나 발생하지 않을 수 있는 모든 시나리오에서 사용됩니다. 이렇게 하면 두 경우 모두에 대해 설명해야 하므로 런타임 문제가 아닌 컴파일 시간 문제가 됩니다. null 대신 "nothing"을 나타내는 데 사용되는 API에서 자주 사용되므로 많은 상황에서 NullReferenceException 대해 걱정할 필요가 없습니다.

module OptionValues =

    /// First, define a zip code defined via Single-case Discriminated Union.
    type ZipCode = ZipCode of string

    /// Next, define a type where the ZipCode is optional.
    type Customer = { ZipCode: ZipCode option }

    /// Next, define an interface type that represents an object to compute the shipping zone for the customer's zip code,
    /// given implementations for the 'getState' and 'getShippingZone' abstract methods.
    type IShippingCalculator =
        abstract GetState : ZipCode -> string option
        abstract GetShippingZone : string -> int

    /// Next, calculate a shipping zone for a customer using a calculator instance.
    /// This uses combinators in the Option module to allow a functional pipeline for
    /// transforming data with Optionals.
    let CustomerShippingZone (calculator: IShippingCalculator, customer: Customer) =
        customer.ZipCode
        |> Option.bind calculator.GetState
        |> Option.map calculator.GetShippingZone

측정 단위

F#의 타입 시스템에는 측정 단위를 사용하여 숫자 리터럴에 컨텍스트를 제공할 수 있는 기능이 포함되어 있습니다. 측정 단위를 사용하면 숫자 형식을 미터와 같은 단위에 연결하고 함수가 숫자 리터럴이 아닌 단위에서 작업을 수행하도록 할 수 있습니다. 이렇게 하면 컴파일러가 특정 컨텍스트에서 전달된 숫자 리터럴 형식이 적합한지 확인할 수 있으므로 이러한 종류의 작업과 관련된 런타임 오류가 제거됩니다.

module UnitsOfMeasure =

    /// First, open a collection of common unit names
    open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames

    /// Define a unitized constant
    let sampleValue1 = 1600.0<meter>

    /// Next, define a new unit type
    [<Measure>]
    type mile =
        /// Conversion factor mile to meter.
        static member asMeter = 1609.34<meter/mile>

    /// Define a unitized constant
    let sampleValue2  = 500.0<mile>

    /// Compute  metric-system constant
    let sampleValue3 = sampleValue2 * mile.asMeter

    // Values using Units of Measure can be used just like the primitive numeric type for things like printing.
    printfn $"After a %f{sampleValue1} race I would walk %f{sampleValue2} miles which would be %f{sampleValue3} meters"

F# Core 라이브러리는 많은 SI 단위 형식 및 단위 변환을 정의합니다. 자세한 내용은 FSharp.Data.UnitSystems.SI.UnitSymbols 네임스페이스를 확인하세요.

개체 프로그래밍

F#은 클래스, 인터페이스, 추상 클래스, 상속등을 통해 개체 프로그래밍을 완전히 지원합니다.

클래스는 .NET 개체를 나타내는 형식이며, 프로퍼티, 메서드 및 이벤트를 멤버로 가질 수 있습니다.

module DefiningClasses =

    /// A simple two-dimensional Vector class.
    ///
    /// The class's constructor is on the first line,
    /// and takes two arguments: dx and dy, both of type 'double'.
    type Vector2D(dx : double, dy : double) =

        /// This internal field stores the length of the vector, computed when the
        /// object is constructed
        let length = sqrt (dx*dx + dy*dy)

        // 'this' specifies a name for the object's self-identifier.
        // In instance methods, it must appear before the member name.
        member this.DX = dx

        member this.DY = dy

        member this.Length = length

        /// This member is a method.  The previous members were properties.
        member this.Scale(k) = Vector2D(k * this.DX, k * this.DY)

    /// This is how you instantiate the Vector2D class.
    let vector1 = Vector2D(3.0, 4.0)

    /// Get a new scaled vector object, without modifying the original object.
    let vector2 = vector1.Scale(10.0)

    printfn $"Length of vector1: %f{vector1.Length}\nLength of vector2: %f{vector2.Length}"

제네릭 클래스 정의도 간단합니다.

module DefiningGenericClasses =

    type StateTracker<'T>(initialElement: 'T) =

        /// This internal field store the states in a list.
        let mutable states = [ initialElement ]

        /// Add a new element to the list of states.
        member this.UpdateState newState =
            states <- newState :: states  // use the '<-' operator to mutate the value.

        /// Get the entire list of historical states.
        member this.History = states

        /// Get the latest state.
        member this.Current = states.Head

    /// An 'int' instance of the state tracker class. Note that the type parameter is inferred.
    let tracker = StateTracker 10

    // Add a state
    tracker.UpdateState 17

인터페이스를 구현하려면 interface ... with 구문 또는 개체 식사용할 수 있습니다.

module ImplementingInterfaces =

    /// This is a type that implements IDisposable.
    type ReadFile() =

        let file = new System.IO.StreamReader("readme.txt")

        member this.ReadLine() = file.ReadLine()

        // This is the implementation of IDisposable members.
        interface System.IDisposable with
            member this.Dispose() = file.Close()


    /// This is an object that implements IDisposable via an Object Expression
    /// Unlike other languages such as C# or Java, a new type definition is not needed
    /// to implement an interface.
    let interfaceImplementation =
        { new System.IDisposable with
            member this.Dispose() = printfn "disposed" }

어떤 유형을 사용할지

클래스, 레코드, 식별 유니온 및 튜플의 존재는 중요한 질문으로 이어집니다. 어떤 것을 선택해야 하나요? 인생의 대부분의 모든 것과 마찬가지로, 대답은 당신의 상황에 따라 달라집니다.

튜플은 함수에서 여러 값을 반환하고 값의 임시 집계를 값 자체로 사용하는 데 적합합니다.

레코드는 명명된 레이블과 선택적 멤버에 대한 지원이 있어, 튜플보다 "단계적 발전"을 이룬 것입니다. 프로그램을 통해 전송 중인 데이터를 간단하게 표현하는 데 적합합니다. 구조적 같음이 있기 때문에 비교에 사용하기 쉽습니다.

구분된 유니온은 다양한 용도가 있지만, 핵심적인 이점은 패턴 매칭과 함께 사용하여 데이터가 가질 수 있는 모든 가능한 "형태"를 고려할 수 있다는 것입니다.

클래스는 정보를 나타내고 해당 정보를 기능에 연결해야 하는 경우와 같은 다양한 이유로 유용합니다. 일반적으로 개념적으로 일부 데이터에 연결된 기능이 있는 경우 클래스와 Object-Oriented 프로그래밍 원칙을 사용하는 것이 중요한 이점입니다. 이러한 언어는 거의 모든 항목에 클래스를 사용하므로 C# 및 Visual Basic과 상호 운용할 때 클래스는 기본 데이터 형식이기도 합니다.

다음 단계

이제 언어의 주요 기능 중 일부를 살펴보았으므로 첫 번째 F# 프로그램을 작성할 준비가 되었습니다. 시작을 확인하고 개발 환경을 설정하는 방법과 코드를 작성하는 방법을 알아보세요.

또한 F# 언어 참조을 통해 F#에 대한 개념 콘텐츠의 포괄적인 컬렉션을 살펴보세요.