F# 배우기 - slog

또 언제 끝날지 모를 여정을 걸어봅니다.

F# 배우기 입니다.

F#이 어떤 언어인가 확인은 위키백과를 참고 합시다.

2005년에 발표한 언어군요. 함수형 언어로만 알고 있었는데 다중 패러다임 언어로 분류 되는군요.

대표 홈페이지가 있습니다.

https://fsharp.org/

6개의 좋아요

F#으로 Blazor를 시작할 수 있을까? 안타깝게도 프로젝트 템플릿 중 F#을 지원하지는 않는 것 같다.

관련해서 Bolero 프로젝트를 찾을 수 있었다.

F#으로 SPA를 개발할 수 있는 환경을 제공한다.

https://fsbolero.io/

3개의 좋아요

개발 환경

개발 환경은 아래 문서(윈도우 환경)를 참고 할 수 있다.

나는 Visual Studio Code + Ionide 환경으로 구축 해보려고 한다.

2개의 좋아요

개발 환경 #2

이유는 모르겠지만 이미 Ionide for F# 플러그인이 설치되어 있다. C# 개발 환경을 Visual Studio Code에 구성하면 F#도 따라서 구성되는 것인가? 이후 정확히 살펴볼 필요가 있겠다.

위의 글을 통해 그대로 따라 해본다.

개발 환경 #3

가상 환경에서 다음처럼 구성하였다.

dotnet new console -lang "F#" -o HelloWorldFSharp
cd HelloWorldFSharp
code .

launch.json은 다음처럼 구성할 수 있다.

{
    "configurations": [

        {
            "type": "coreclr",
            "request": "launch",
            "name": "Launch HelloWorldFSharp",
            "program": "${workspaceFolder}/bin/Debug/net8.0/HelloWorldFSharp.dll",
            "args": [],
            "cwd": "${workspaceFolder}",
            "stopAtEntry": false,
            "console": "internalConsole"
        }
    ]
}

Start Debugging으로 breakpoint가 잘 적용되는 것을 확인할 수 있다.

F#의 Visual Studio Code 구성이 원활하지 않아 (관련해서 Visual Studio Code로 F#을 코딩하시는 분께는 환경 구성을 어떻게 했는지 댓글로 도움 바랄께요) Visual Studio 환경에서 하는 것으로 변경한다.

F# 관련 국내 글

이미 5년 전의 글이긴 하지만 내용이 좋아 공유 합니다.

2개의 좋아요

F#으로 만든 국내 프로젝트

https://csrc.kaist.ac.kr/blog/2020/08/12/바이너리-분석에-웬-fsharp/

https://kwangkeunyi.snu.ac.kr/~kwang/paper/maso/1.html

B2R2는 F#으로 만들어진 이진 분석을 위한 알고리즘, 함수 및 도구 모음 – 라이브러리 입니다.

1개의 좋아요

접근 문서

특히 Cheat Sheet의 경우 나같은 초보자에게는 F#을 파악하기에 유용한 문서일 것 같다.

1개의 좋아요

Fable - F#을 이용해서 JavaScript 등의 언어로 변환하는 컴파일러.

제가 전에 한번 소개했었군요.

그렇기 때문에 REPL도 웹 페이지로 라이브로 사용할 수 있습니다.

1개의 좋아요

F#을 배워야 하는 이유

물론 좋다고 다수(의 프로그래머)가 사용 할 무엇이 된다는 것은 아닐꺼에요. 어쨌든 이 글을 통해 F#의 장점을 파악할 수 있습니다.

2개의 좋아요

C# 개발자가 F#을 배워야 하는 이유

라는 제목으로 학습 목적 및 컨텐츠 생성 목표를 설정해야겠다. 우리나라에서 프로그래머가 되려는 일반 학생이 F#을 배우려 할 가능성은 0% 수렴. 왜… F#는 커녕 Unity로 게임 개발하려고 하는 소수의 학생 빼고는 C#을 배울 생각도, 의지도 없고 있는지조차 모른다. F#으로 취업하기는 하늘에 별 따기일 수 있다. F#은 아마도 본인 연구에 활용할 함수형 프로그래밍에 관심을 가진 극소수의 학/석사의 학생들이 접하고자 하는 언어일 것이다.

실제로 취업은 삶에서 중요한 이벤트이다

함수형 언어 및 F#에 대해 흥미와 감을 조금씩 잡게 도운 사이트는 아래와 같다.

대부분의 컨텐츠가 2015년 이전의 글들이라 쪼금 아쉽기는 하지만 학습 후 F#이 어떻게 변화되었는지를 살펴봐야곘다. 그것 빼고는 훌륭한 내용이다.

예를 들어

The 'Why use F#' series | F# for fun and profit

이 글을 쭉 살펴보면 F#에 대해 감을 잡을 수 있다. 이 글 외에도 훌륭한 글들이 많다.

3개의 좋아요

F# 배우기 (feat. 함수형 프로그래밍)

명령형 개념을 가지고 접근하지 말자

명령형 개념이란 우리가 지금까지 접했던 C/C++/Java/JavaScript/…C#! 언어들이 접근하는 방식으로 코드가 곧 실행되어 결과를 만드는 개념을 말한다. 순수 함수형 언어는 변수(변하는 저장소)도 없고, 루프(반복문)도 없고, 객체도 없다.

물론 F#은 순수 함수형 언어는 아니다. .NET 생태계를 활용해야 하므로 객체지향 용법도 있다.

그렇다면 값이 변하지 않고(불변), 루프도 없고(재귀), 객체도 없고(함수) 어떻게 문제를 풀 수 있을까?

솔직히 나도 아직까지는 잘 모르겠다. 배우고 있는 중이다.
뭐 추측으로는 순수 함수형을 지향하며 예측되는(불변) 함수의 조립으로 문제를 푸는 것이리라

명령형과 반대되는 개념은 선언형이다. 다음의 글을 살펴보자.

그렇다고 한다. 훌룽하게 정리하셨는데 조금 더 글이 짧아도 될뻔했다.

생각하는 방식을 바꾸자

객체지향 프로그래밍이 이전의 절차지향(명령형) 프로그래밍과 완전히 다른 개발 관점이 필요한 것 처럼 함수형 프로그래밍은 문제 해결의 접근 방식이 객체지향 프로그래밍과 다르다.

명령형이 아닌 함수형 스타일로 코딩하려면 함수적 사고가 필요하다. 아래의 시리즈를 살펴보자.

해야 할 일과 하지 말아야 할 일

mutable은 초기에는 사용하지 말자.

객체지향 언어를 절차지향 처럼 쓸 수 있는 것 처럼 F#은 비함수형 언어처럼 쓸 수 있다. 값이 mutable이 되지 않도록 초기에는 지양하자.

forif-then-else를 쓰지 말자

재귀와 패턴 매칭을 쓰자.

"hello".length 를 쓰지 않고 String.length "hello"를 대신 쓰자

개체의 사용이 아니므로 그렇다고 한다. (정확한 건 좀 더 학습한 뒤 수정하는 것으로)

클래스를 만들지 말자

이 말은 역으로 F#이 클래스를 만들 수 있다는 것이다. 어쨌든 순수 함수형을 지향에서 튜플, 레코드, 유니언 등으로 문제를 풀어야 한다.

디버거를 사용하지 말자

F#은 컴파일러가 좀 더 엄격하므로 (이것도 더 학습한 뒤 수정하는 것으로)

반면에:

작은 type 특히 union type을 권장함

가볍고 쉬우며, 도메인 모델을 쉽게 문서화하고 정확성을 보장한다.

listseq유형과 관련해서 라이브러리 모듈을 숙달함

List.foldList.map등의 함수는 강력하다. 이해하면 다양한 곳에서 활용할 수 있다.

컬렉션 모듈을 이해했다면 재귀를 피해야 함

꼬리 재귀인지 확인해야 하며 대부분 List.fold로 문제를 풀 수 있음

가능한 파이프(|>)와 컴포지션(>>)을 활용함

F#은 f(g(x))보다 깔끔하게 파이프와 컴포지현으로 표혈할 수 있다.

부분 적용을 이해하고 암묵적 스타일에 익숙해지자

(… 무슨 말인지 모르겠음 이해하고 수정할 것)

대화형 창을 이용해서 코드 조작을 테스트하자

2개의 좋아요

부분적용 전에 커링을 먼저… :melting_face:

조금 더 의견 부탁드릴께요 ^^ 편하게 말씀해주세요~

1개의 좋아요

C#에 비해 F#의 주요 이점

  • 간결함 : 중괄호, 컴마, 세미콜론 등의 노이즈?와 유형 추론 시스템으로 코드가 간결해 짐
  • 편의성 : 복잡한 유형을 쉽게 정의하고 다른 함수를 매개변수로 갖거나 기존 함수를 결합하여 새로운 기능을 쉽게 만들 수 있음
  • 정확성 : null 참조 예외 등의 일반적인 오류를 방지하는 강력한 유형 시스템이 있으므로 컴파일 타임에 유형 오류로 대부분의 오류를 잡을 수 있음
  • 동시성 : 데이터 구조가 불변이기 때문에 상태를 공유하고 잠금을 피하는 자연스런 동시성을 지원함. 동시성 관련 다양한 내장 도구와 라이브러라리가 있음
  • 완전성 : F#은 함수형 언어일 뿐만 아니라 다른 스타일 또한 지원하기 때문에 C#에서 할 수 있는 거의 모든 것을 할 수 있음

… 그렇다고 합니다. 아직은 전반적인 언어의 특징 및 사용법을 이해한 단계가 아니기 때문에 흔적으로 기록…

2개의 좋아요

F# 문법 익히기 (60초)

C#과의 중요한 차이점

  • 중괄호 대신 띄어쓰기
  • 매개변수를 구분하기 위해 쉼표 대신 공백 사용
  • 함수는 반드시 사용 전에 정의되어야 함
let myInt = 5
let myFloat = 3.14
let myString = "hello"

기본적으로 불변으로 정의 됩니다.

let twoToFive = [2; 3; 4; 5]
let twoToFive2 = [
    2
    3
    4
    5
]
let twoToFive3 = [2..5]
let twoToFive4 = [for i in 2..5 -> i]


printfn "%A" twoToFive
printfn "%A" twoToFive2
printfn "%A" twoToFive3
printfn "%A" twoToFive4

결과는? 모두 [2; 3; 4; 5] 이다.
(물론 어떤 것이 연산 결과가 컴파일 타임인지 런타임인지는 아직은 잘 모르겠다.)

그리고… [1;2;3] 이런 표현은 일반적인 연속 배열이 아니라 링크드리스트이다!
(물론 내부적으로 연속 배열들의 집합이 되도록 최적화 되긴 할 것이다. 잘 모르겠다)

일반 연속 배열은 [|1; 2; 3|] 라고 사용한다.

let oneToFive = 1 :: twoToFive
printfn "%A" oneToFive

앞에 1을 추가하여 새로운 목록을 생성한다. 새로 생성한 목록은 내부적으로 복사가 아니라 링크드리스트로 참조하여 빠르게 생성한다.

let zeroToFive = [0;1] @ twoToFive

두 목록을 연결해서 새로운 목록을 만들 수도 있다.

let square x = x * x
square 3

let add x y = x + y
add 2 3

함수를 이렇게 만들고 사용할 수 있다.

let evens list =
    let isEven x = x%2 = 0
    List.filter isEven list

evens oneToFive

함수 안에 함수를 사용해서 코드를 줄이거나 가독성을 올릴 수 있다.

let sumOfSquaresTo100 =
    List.sum ( List.map square [1..100] )

이렇게 함수를 만들 수 있지만 파이프를 이용하는 것이 좀 더 자연스럽다.

let sumOfSquaresTo100piped =
   [1..100] |> List.map square |> List.sum

square 함수 대신 람다 함수로 바로 표현할 수 도 있다.

let sumOfSquaresTo100withFun =
   [1..100] |> List.map (fun x->x*x) |> List.sum

패턴 매칭은 원래 F#에서 제공되던 것이다.

let simplePatternMatch =
   let x = "a"
   match x with
    | "a" -> printfn "x is a"
    | "b" -> printfn "x is b"
    | _ -> printfn "x is something else"

F#에서는 Option 타입에 의해 값이 있을수도 있고 없을 수도 있는 상태를 좀 더 명확하게 표현할 수 있다.

let validValue = Some(99)
let invalidValue = None

Option으로 다음과 같이 패턴 매칭을 할 수 있다.

let optionPatternMatch input =
   match input with
    | Some i -> printfn "input is an int=%d" i
    | None -> printfn "input is missing"

optionPatternMatch validValue
optionPatternMatch invalidValue

튜플은 컴마로 표현한다.

let twoTuple = 1,2
let threeTuple = "a",2,true

레코드 유형은 이름 필드로 세미콜론으로 구분하여 표현한다.

type Person = {First:string; Last:string}
let person1 = {First="john"; Last="Doe"}

C#에서 그렇게 사용하고 싶은 유니온 유형이다.

type Temp =
  | DegreesC of float
  | DegreesF of float
let temp = DegreesF 98.6

Temp 유형은 DegreesC일 수도 있고 DegreesF일 수도 있다.

다음과 같이 유형은 재귀적으로 정의될 수 도 있다.

type Employee =
  | Worker of Person
  | Manager of Employee list
let jdoe = {First="John";Last="Doe"}
let worker = Worker jdoe

출력은 %A을 줘서 값을 이쁘게 출력할 수 있다.

printfn "Printing an int %i, a float %f, a bool %b" 1 2.0 true
printfn "A string %s, and something generic %A" "hello" [1;2;3;4]

printfn "twoTuple=%A,\nPerson=%A,\nTemp=%A,\nEmployee=%A"
         twoTuple person1 temp worker
6개의 좋아요

F#과 C# 비교 : 간단한 합계

C# 개발자가 F#의 간단 명료한 코드의 매력을 느낄 수 있도록 계속 분석합니다.

슬로그 중의 타이틀과 원 내용은 https://fsharpforfunandprofit.com/ 의 내용입니다.

F#으로 1부터 n까지의 제곱의 합을 구하는 코드는 무엇일까요?

let square x = x * x

let sumOfSquares n =
    [1..n] |> List.map square |> List.sum

let result = sumOfSquares 100
printfn "%A" result

이에 해당하는 C# 코드는 무엇일까요?

public static class SumOfSquaresHelper
{
   public static int Square(int i)
   {
      return i * i;
   }

   public static int SumOfSquares(int n)
   {
      int sum = 0;
      for (int i = 1; i <= n; i++)
      {
         sum += Square(i);
      }
      return sum;
   }
}

F#은 C#에 비해 다음의 특징이 있습니다.

  • 더 컴팩트 합니다.
  • 유형 선언이 없습니다.
  • 대화형 개발이 가능합니다.

물론 최신 C#은 위의 코드 보다 좀 더 F# 처럼 가능합니다.

var square = (int x) => x * x;

var sumOfSquares = (int n) =>
    Enumerable.Range(1, n).Select(square).Sum();

var result = sumOfSquares(100);
Console.WriteLine(result);

LINQ와 그 Fluent API 형태, 람다 등 함수형 언어에서 많이 영향 받았다라는 것을 알 수 있군요!

C# 코드가 좀 더 간결해지긴 했지만 여전히 F# 코드가 좀 더 간결한 것 같습니다. 어쨌든 C#도 많이 간결해졌네요!

3개의 좋아요

F#과 C# 비교 : 정렬

퀵소트를 통해 F#의 구현과 C#의 구현 차이를 살펴봅시다!

알고리즘은 언어 별 구현된 코드를 비교하는 목적으로 단순화 되었습니다.

다음으로 퀵소트 로직을 설명할 수 있습니다.

  • 목록이 비어 있으면 할 일이 없음. 그렇지 않은 경우,
  • 목록의 첫 번째 요소를 가져옴
  • 나머지 목록에서 첫 번째 요소보다 작은 요소를 모두 찾아서 정렬
  • 목록의 나머지 요소 중 첫 번째 요소보다 크거나 같은 요소를 모두 찾아서 정렬
  • 세 부분을 합쳐서 최종 결과를 얻음 (정렬된 작은 요소 + 첫 번째 요소 + 정렬된 큰 요소)

이 알고리즘을 F# 코드로 변환하면 다음과 같습니다.

let rec quicksort list =
    match list with
    | [] -> []
    | firstElem::otherElements ->
        let smallerElements =
            otherElements
            |> List.filter (fun e -> e < firstElem)
            |> quicksort
        let largerElements =
            otherElements
            |> List.filter (fun e -> e >= firstElem)
            |> quicksort
        List.concat [smallerElements; [firstElem]; largerElements]

printfn "%A" (quicksort [1;5;23;18;9;1;3])

이에 반해 C# 코드는 다음과 같습니다.

public class QuickSortHelper
{
   public static List<T> QuickSort<T>(List<T> values)
      where T : IComparable
   {
      if (values.Count == 0)
      {
         return new List<T>();
      }

      T firstElement = values[0];

      var smallerElements = new List<T>();
      var largerElements = new List<T>();
      for (int i = 1; i < values.Count; i++)
      {
         var elem = values[i];
         if (elem.CompareTo(firstElement) < 0)
         {
            smallerElements.Add(elem);
         }
         else
         {
            largerElements.Add(elem);
         }
      }

      var result = new List<T>();
      result.AddRange(QuickSort(smallerElements.ToList()));
      result.Add(firstElement);
      result.AddRange(QuickSort(largerElements.ToList()));
      return result;
   }
}

알고리즘 로직을 코드로 전환한 것은 C# 코드에 비해 F# 코드가 더 나아 보입니다. (물론, C#의 LINQ를 사용하지 않았기 때문에 조금은 불공평한 비교 같기는 하네요)

다음 코드는 LINQ와 확장 메서드를 이용한 좀 더 현대적(함수형 스타일)인 C# 입니다.

public static class QuickSortExtension
{
    public static IEnumerable<T> QuickSort<T>(
        this IEnumerable<T> values) where T : IComparable
    {
        if (values == null || !values.Any())
        {
            return new List<T>();
        }

        var firstElement = values.First();
        var rest = values.Skip(1);

        var smallerElements = rest
                .Where(i => i.CompareTo(firstElement) < 0)
                .QuickSort();

        var largerElements = rest
                .Where(i => i.CompareTo(firstElement) >= 0)
                .QuickSort();

        return smallerElements
            .Concat(new List<T>{firstElement})
            .Concat(largerElements);
    }
}

참고로 다음처럼 F# 코드를 좀 더 간결하게 표현할 수 있습니다.

let rec quicksort2 = function
   | [] -> []
   | first::rest ->
        let smaller,larger = List.partition ((>=) first) rest
        List.concat [quicksort2 smaller; [first]; quicksort2 larger]

F# 코드가 C# 코드보다 나은 이유는 로직을 거의 그대로 코드로 변환할 수 있다는 점으로 선언적(“무엇을 해야 하는지”)에 더 집중할 수 있게 하고 명령적(“어떻게 해야 하는지”)에 덜 집중하게 합니다.

1개의 좋아요

왠지 C#이 진 듯하여 ^^; (죄송)

public static class IEnumerableExtensions
{
    public static IEnumerable<T> QuickSort<T>(this IEnumerable<T> source)
        where T : IComparable<T>  =>  source.Any() is false ? []
        : [
            ..source.Skip(1).Where(x => x.CompareTo(source.First()) < 0 ).ToList().QuickSort(),
            source.First(),
            ..source.Skip(1).Where(x => x.CompareTo(source.First()) >= 0 ).ToList().QuickSort()
        ];    
}
2개의 좋아요