본문 바로가기
프로그래밍/함수형 프로그래밍

C#으로 함수형 프로그래밍을 해보자

by bantomak 2024. 3. 14.

C#으로 함수형 프로그래밍을 해보자

분명히 하고 가야 할 부분이 있다. C#에서 함수형 프로그래밍에서 영감(functional-programming-inspired)을 받아서 만들어진 기능(feature)들은 순수 함수형 언어의 기능과 완전히 동일하지 않다. (예를 들면 하스켈, Haskell)

 

하지만 함수형 프로그래밍의 개념을 이해하고 이를 채용하는 것만으로도 앞으로 우리가 적성하는 코드의 질을 대폭 향상할 수 있을 것이다.

 

C#에서 왜 함수형 프로그래밍을?

.NET 프레임워크는 몇몇 함수형 기능들을 LINQ 확장자 메서드로 지원한다. 함수형 개념이 들어가 있는 걸 인지하지 못하고 사용했을 것이다. 이제 우리는 C#의  함수형 기능들을 통해서 함수형 프로그래밍이 가지고 있는 장점을 코드에 적용해 볼 것이다.

 

일단 두 가지 개념에 대해서 알아보자.

  • 순수 함수(Pure functions)
  • 불변 데이터(Immutable data)

 

순수 함수(Pure functions)는 외부 세계와 완전히 격리되어 있다. 오로지 전달받은 파라미터만으로 소통한다. 그래서 부수효과(Side Effect)가 발생하지 않는다. 그래서 예상하기 쉬우며, 이해하기 쉽다. 고로 테스트하기에도 편하다.

 

불변 데이터(Immutable data)는 한번 초기화되고 나면 수정이 불가능한 객체(object)나 자료구조(data structures)를 말한다. 상태가 변경되지 않기 때문에 이해하기 쉽고 그로 인해 스레드 세이프(thread-safe)하다.

 

C# 개발자들이 자주 사용하는 System.String 클래스는 유명한 불변 객체이다.

 

 

C# String은 참조 타입(Reference type)이면서 불변(Immutable)하다.

string vs StringBuilder 예를 들어, int의 경우를 살펴보자. int는 값 타입(value type)이면서 가변(mutable)이다. 값 타입은 데이터 변경 시에 새로운 메모리 할당이 일어나지 않는다. 이미 생성된 메모리의

jettstream.tistory.com

 

우리가 몰랐던 C# 함수형 프로그래밍

함수형 프로그래밍의 기본이 되는 함수들이 있다.

  • map
  • filter
  • reduce

위의 3가지 함수들이 C#에서 어떻게 구현되는지 살펴보자.

 

C# Map

map은 간단히 말하자면 시퀀스의 요소를 인자로 받아서 변환하고 그 결과를 새로운 시퀀스로 반환한다.

즉, map을 통해서 구조를 바꾸거나 타입을 변경할 수 있다.

 

전통적인 구현

static void AddThreeToEachElement(int[] arr)
{
    for (var i = 0; i < arr.Length; i++)
    {
       arr[i] += 3;
    }
}

 

전통적인 구현의 문제점은

  • 구현이 너무 구체적이다.
  • 상용구들이 너무 많다.

F# 코드

let result = Seq.map (fun x -> x + 3) numbers

 

훨씬 간단한 코드가 되었다. map은 seq의 인자를 하나씩 받아서 + 3을 해준다. 그뿐이다.

 

C# 코드

var result = numbers.Select(x => x + 3);

 

익숙한 코드가 나왔다. 기존에 많이 쓰던 Select를 사용하면 함수형 프로그래밍 map을 그대로 구현할 수 있다.

추가적으로 map은 요소를 변형시키는 것을 넘어서 해당 타입도 변경이 가능하다.

 

List<Employee> employees = EmployeeRepository.All();
IEnumerable<int> ids = employees.Select(x => x.Id);

 

C# Filter

filter는 아주 직관적인 이름을 가지고 있어서 바로 유추가 가능할 거라고 생각한다. 시퀀스에서 원하는 요소들을 찾아서 새로운 시퀀스로 반환하는 것이다.

 

전통적인 구현

public static List<Employee> GetEmployeesWithAtLeastNSickdays(List<Employee> employees, int number)
{
    List<Employee> result = new List<Employee>();
    
    foreach (var e in employees)
    {
        if (e.Sickdays >= number)
     {
      result.Add(e);
     } 
    }
    
    return result;
}

 

C# 코드

public static List<Employee> GetEmployeesWithAtLeastNSickdays(List<Employee> employees, int number)
{
    return employees.Where(x => x.SickDays >= n).ToList();
}

 

Where을 사용하면 함수형 프로그래밍 filter를 구현할 수 있다.

 

C# Reduce

다른 이름으로는 Fold가 있다.

 

많은 개발자들이 Reduce에 대해서 헷갈려하는데 아래와 같이 접근해 보길 추천한다.

시퀀스가 있고 요소들을 반복적으로 인자로 넣어서 호출하는 함수가 있다. 그리고 이 함수의 결괏값은 계속해서 축적된다.

 

전통적인 구현

public int Sum(IEnumerable<int> numbers)
{
    var result = 0;
    foreach (var number in numbers)
    {
        result += number;
    }

    return result;
}

 

C# 코드

var sum = number.Aggregate((x, y) => x + y);

 

물론 더 간단한 방법으로는 Sum을 사용하는 방법이 있다.

 

var sum = numbers.Sum();

 

그러면 Aggregate를 사용하는 이유는 무엇일까?

 

Aggregate를 사용하면 단순히 int뿐만이 아니라. 어떠한 이항 연산(Binary Operation)에 사용이 가능하다. 문자열이나 커스텀 타입에도 적용이 가능하다.

 

정리하자면

많은 사람들이 LINQ가 함수형 프로그래밍의 개념을 가져와서 구현되었다는 것을 모르고 사용한다. 물론 모르고 사용해도 상관없다. 하지만 알고 사용하는 것과 모르고 사용하는 것에는 많은 차이가 있다고 생각한다.

 

람다에 대해서 공부하고 함수형 프로그래밍의 개념과 관련 수학적인 이론을 익히면 더 나은 C# 코드를 작성할 수 있을 것이다. 이는 분명한 사실이다. 이번 기회에 함수형 프로그래밍에 대해서 관심을 가져보자. 아마도 새로운 즐거움을 선사할 것이다.

 

함께 읽으면 좋은 글

 

문 스타일(statement style) vs 식 스타일(expression style)

문 스타일(statement style) 결과를 도출하기보다 행위를 정의하는 것은 명령형 프로그래밍 기법의 하나다. 즉, 컴퓨터에게 무엇을 해야 할지 이야기하는 방식으로 소스 코드를 이 관점에서 설명해

jettstream.tistory.com

참조 사이트

 

Functional Programming in C#: Map, Filter, and Reduce

You can do functional programming in C#. Learn C# Map, Filter, and Reduce in order to help you write cleaner code that's easier to reason about.

blog.submain.com

댓글