F# 둘러보기

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

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

온라인으로 코드 실행

컴퓨터에 F#이 설치되어 있지 않은 경우 F#을 F#(F)로 사용하여 브라우저 에서 모든 샘플을 실행할 수 있습니다. 우화는 브라우저에서 직접 실행되는 F#의 방언입니다. REPL에서 이어지는 샘플을 보려면 Fable REPL의 왼쪽 메뉴 모음에서 샘플 학습 > F#을 검사.>

함수 및 모듈

모든 F# 프로그램의 가장 기본적인 요소는 모듈로 구성된 함수입니다. 함수는 입력 작업을 수행하여 출력을 생성하며 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 의해야 합니다. 참조와 구조체 튜플 간에 명시적으로 변환해야 합니다.

Pipelines

파이프 연산자 |>는 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에 익숙하다면 구조적 평등과 의식 감소만으로 POKO 또는 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 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는 재귀 정의를 지원하므로 트리와 본질적으로 재귀 데이터를 쉽게 나타낼 수 있습니다. 예를 들어 함수와 insert 함께 exists 이진 검색 트리를 나타내는 방법은 다음과 같습니다.

/// 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# 코어 라이브러리의 일부일 때 매우 유용합니다.

옵션 형식 은 두 가지 경우 중 하나를 나타내는 형식입니다. 값 또는 전혀 없는 경우입니다. 값이 특정 작업으로 인해 발생하거나 발생하지 않을 수 있는 모든 시나리오에서 사용됩니다. 이렇게 하면 두 경우 모두에 대해 설명해야 하므로 런타임 문제가 아닌 컴파일 시간 문제가 됩니다. 이러한 API는 "nothing"을 나타내는 데 사용되는 API null 에서 자주 사용되므로 많은 상황에서 걱정할 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" }

사용할 형식

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

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

레코드는 명명된 레이블과 선택적 멤버에 대한 지원이 있는 튜플의 "단계별 실행"입니다. 프로그램을 통해 전송 중인 데이터의 낮은 의식 표현에 적합합니다. 구조적 같음이 있기 때문에 비교에 사용하기 쉽습니다.

차별된 공용 구조체는 많은 용도를 사용하지만 핵심 이점은 패턴 일치와 함께 이를 활용하여 데이터가 가질 수 있는 모든 가능한 "셰이프"를 고려할 수 있다는 것입니다.

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

다음 단계

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

또한 F# 언어 참조검사 F#에서 개념 콘텐츠의 포괄적인 컬렉션을 확인합니다.