728x90

패턴 매칭이란 호출해야 할 함수의 변형을 정확하게 선택하는 디스패치의 유형이다

 

입력 값을 전달하고 정확한 선택을 결정하는 if 조건식의 개념으로 생각하면 되는것 같다.

 

c#의 Nullable<T>, IEnumerable<T>, Func<T>, Lazy<T>, Task<T> 형식은 모나드라는 개념을 구현하고 있다.

 

1. 모나드는 다른 타입을 인자로 받는 타입이다.

모나드는 기본적으로 int나 string 같은 타입이고, 타입을 인자로 받는다고 한다.

c++의 템플릿, c# 제네릭 클래스를 생각하면 되고, 이 T라는 타입의 인자를 받는 모나드 M을 M[T] 라고 표현을 한다.

return operator 혹은 unit operator라고 부른다.

 

2. 모나드는 타입의 값을 생성하는 함수가 있어야 한다

모나드는 임의 타입의 값을 받아서 그 타입을 인자로 받은 모나드 타입의 값을 반환하는 함수가 있어야 한다.

T 타입의 값을 받아서 M[T] 타입의 값을 반환하는 함수가 있어야 한다.

보통 bind operator라고 부른다.

 

3. 다른 모나드 타입으로 진행하는 함수가 있어야 한다.

M[T] 타입의 모나드가 있을 때 T타입의 변수를 받아 M[U] 타입의 모나드를 반환하는 함수를 받아서, M[U] 타입의 값을 반환하는 함수다.

이 함수를 통해서 모나드에서 다른 모나드로 진행 할 수 있다.

 

위 3가지 조건을 만족해야 모나드라는 패턴이 만족 된다고 한다. 

 

...........프로그래머 로써는 이정도로만 이해해도 된다고 하는데 그래도 이게 무슨 말인지 알수가 없구나...

 

사용 예제 : Option : 존재하지 않음을 표현하기 

(출처 : https://blog.seulgi.kim/2015/07/monad-option.html)

Option 타입이 가장 기본적인 모나드이고, 많이 사용되는 모나드라고 한다.

 

Option 타입이 해결하고자 하는 문제는 값이 존재하지 않음을 런타임 에러가 발생할 가능성 없이 표현하는 것이다.

 

C++, C# 등 기존의 많은 언어는 값이 존재하지 않음을 표현하기 위해서 null point를 사용하는데 이 null point는 컴파일 타임에 잡을 수 없는 nullpointerException을 발생 시키기 떄문에 조심해야 한다.

 

이런 문제를 null object pattern 같은 패턴을 이용하거나 null check를 한 겹 감싼 클래스를 만들거나 해서 보완하지만 문제를 완벽하게 해결할 수는 없다

 

하지만 Option 타입은 이에 대한 해결책을 제공한다.

 

Option 타입은 하나의 타입 파라미터를 받아, 그 타입의 값을 가지고 있을 수도 있고, 없을 수도 있다. Int 타입을 타입 파라미터로 받았다면, 타입은 Option[Int]가 되며, String 타입을 타입 파라미터로 받았다면, Option[String]이 된다. 즉, T 타입을 타입 파라미터로 받은 Option은 Option[T]가 된다. 이를 간단히 표현하기 위해서 T?같은 방식으로 표현하기도 한다.

Option[T] 타입의 값은 T 타입의 값을 가지고 있을 수도 있고, 아무런 값이 없을 수도 있다. 이렇게 말하면 단순한 nullable과 다를 게 없어 보인다. 하지만 Option은 두 상태를 다른 타입으로 분리함으로써 nullable보다 안전한 방법을 제공한다.

Option 타입은 두 타입의 sum type이다. 하나는 값이 존재하지 않음을 나타내는 None이라는 타입이고, 다른 하나는 무언가 값이 있음을 나타내는 Some이라는 타입이다. sum type을 지원하는 F#, rust, Haskell 같은 언어에서는 이를 sum type으로 표현하고, Scala나 전통적인 객체지향 언어에서는 Option이라는 interface의 구현체로 Some과 None이 있는 것으로 표현한다.

 
None은 아무런 값도 가지고 있지 않음을 나타내는 타입이다. None 타입의 값에 bind operator를 호출해도 아무 일도 일어나지 않는다. 인자로 넘겨진 함수는 실행되지 않고, bind operator의 결과는 언제나 None이다.
 
 Some 타입은 무언가 값을 가지고 있음을 나타내는 타입이다. 어떤 타입의 값을 가졌는지 나타내기 위해 타입 파라미터를 받는다. T 타입의 값을 가지고 있는 Some 타입은 Some[T]라고 표현한다. Some[T] 타입은 반드시 T 타입의 값을 들고 있어야 한다. Some 타입이면서 내부적으로 값을 들고 있지 않는 상황은 올 수 없다.1) Some타입은 반드시 값을 가지고 있기 때문에 bind operator는 언제나 NullPointerException 없이 원하는대로 실행된다.
 

책을 보면서도 처음 보는 개념이라 많이 이해하기 힘들고..

내가 이해한 것이 맞는건지는 모르겠지만. 정리를 해보자면

 

모나드가 바라는 것은 함수 A의 결과를 함수 B에 전달해서 수행을 하고 싶은데 

함수마다 정의된 리턴값 타입이 다를수도 있고, 

A의 결과를 특정 공간에 저장한 다음, 함수 B가 해당 공간에 접근해서 수행을 진행하는 방법이 있다고 한다면

이 방법은 A, B 함수가 아닌 또다른 외부 함수가 공간에 저장할 가능성이 있고 그렇다면 완벽한 결과를 보장할 수 없기 때문에 함수형 프로그래밍에서 원하는 방향과는 맞지 않는다는 것이고

이것을 해결하기 위해 함수 A와 B를 합친다? 라고 해야할까,, 하나의 함수인 것처럼 동작하게 하는 함수를 만든다는 것이 모나드의 개념인 거 같다.

 

추가 한줄 요약 정리 : <T> 타입의 Functor를 <R> 타입의 Functor로 바꾸는 기능

 

책에있는 간단한 예제로 보면

string타입을 받아서 string에 해당하는 지정된 숫자 int 타입을 반환하는 함수가 있다

private static Nullable<int> WordToNumber(string word)
{
  Nullable<int> returnValue;
  if ( word == null ) return null;
  switch (word.ToLower())
  {
    case "zero" : returnValue = 0; break;
    case "one" : returnValue = 1; break;
                     .....
    default: returnValue = null;
  }
int 형식은 null을 처리하지 못하지만, Nullable<int>를 사용해서 null을 반환 할수있게 한것이다.

 

WordToNumber에는 우리가 따로 if문을 사용해서 if (word == null) 이냐 인것처럼 null 체크가 불필요하다.


null일 경우에는 그냥 null을 반환하고 값이 null이면 로직이 실행이 되지 않는다거나 하게끔 로직을 짜서 타입에 대한 안정성을 유지 할 수 있다는 것


이런 예외적인 값에 대한 컨트롤이 가능한 로직을 짤수 있다는 방식 때문에
비동기 데이터를 다루는 로직에 있어서도 값이 이미 존재하는 것 처럼 로직을 짤 수 있을것 같다.


중간에 예외 발생 없이 함수형 프로그래밍에서 추구하는 입력에 따른 결과를 반환하는 것을 보장하는 방법이 되는 것이 모나드 패턴인거 같다.

 

혼자 프로그래밍 작업을 하는 환경이라면 모르겠지만.. 여러 사람들과 같이 하는 작업중에 이러한 패턴을 이용해서 함수를 설계 할때는 사전에 협의가 잘 되어 있어야 가능할 것 같고, 

합친다, 확장한다라는 개념은 물론 좋지만 이것도 결국엔 몸집이 계속해서 커져 나간다면 .. 어떻게 관리되어지고 어떻게 유지 보수를 해 나갈지, 이런 걱정도 많이 해보게 될 것 같다. 

 

 

728x90
Posted by 정망스
,
728x90

프로그래밍에서 지연이란 특정 코드의 실행을 연기하는 것을 뜻한다.

 

-지연 열거

C#에서 컬렉션 데이터를 열거하는 IEnumerable<T>는 지연 평가를 이용한다.

배열과 List<T>도 IEnumerable<T> 인터페이스를 구현하지만 데이터로 채워져야 하기 때문에 사전 평가가 이뤄져야 한다.

Enumerable.Skip(n) : 지정 된 개수의 시퀀스의 요소를 무시 하고 나머지 요소를 반환

Enumerable.Take(n) : 시퀀스의 시작 부분부터 지정 된 수의 연속 요소를 반환

IEnumerable이 지연 평가로 실행이 되기 때문에 foreach()문에서 컬렉션을 순회하면서 GetEnumerator()를 이용한 IEnumerator<T>형식에 존재하는 MoveNext() 메서드 같은 경우엔 결과 계산이 필요한 경우에만 호출이 된다. 

 

-지연 평가

if (data != null || data.count > 1) 이라는 조건 구문이 있을때

OR 연산자는 

data != null 이 false인 것을 확인하면 data.count > 1을 평가한다 하지만 data가 null이므로 count 속성에 접근이 불가하여 예외가 발생한다.

 

f (data != null && data.count > 1) 이라는 조건 구문이 있을때

AND 연산자

data != null이 false인 것을 확인하면 나머지 식을 판단하지 않고 지연시켜서 && 논리 연산의 결과를 false로 결론 내어 예외가 발생하지 않는다. 이런 지연 평가를 이용하면 하나의 식만으로 결론을 판단하고 나머지 식을 무시할 수 있다.

 

-엄격하지 않은 평가

4 + ( 3 * 2 ) 계산식을 작성할때 엄격한 평가는 (3 * 2)를 먼저 계산한 결과를 가지고 후에 + 4를 해주는 과정을 거치며

엄격하지 않은 평가는 + 연산자가 먼저 처리되고 다음으로 내부 공식인 (3 * 2)가 처리 되는 방식

평가 순서가 바깥에서 안쪽으로 향한다.

 

-지연 초기화

사용 시점까지 개체 생성을 연기하는 최적화 기법

멤버에 대한 액세스가 이뤄지기 전에는 초기화 되지 않는 개체를 정의하는 것이다.

C#에서는 Lazy<T> 클래스가 있다.

 

Lazy<Data> data = new Lazy<Data>(() => new Data());

Lazy를 이용하면 위와같이 data 변수를 정의한 후에도 초기화가 이뤄지지 않는다.

IsValueCreated 속성을 이용해서 data 변수가 초기화 되었는지 확인 가능하고

Value 속성을 이용해 인스턴스의 값을 가져올수 있다.

 

(data.Value as Data).Count

멤버 Count에 대한 접근을 하려 하면 값을 가져오기 위해 내부적으로 Data의 생성자를 먼저 호출하여 초기화를 한 후

Count 값을 가져오게 된다.

 

-지연의 장단점

장점

불필요한 기능을 위한 초기화 시간 절약 가능실행 
순서의 중요성이 떨어지는 경우로 프로그램을 더 효과적으로 실행 
가능지연을 이용해 보다 효율 적인 코드 작성 가능

단점

프로그램 흐름을 예측하기 어렵고 제어가 어려워 질 수 있다
지연을 구현하는 코드의 복잡성에 따른 성능 하락이 발생할 수 있다.

 

-사전 연산

캐시 기법의 하나로 조회 테이블을 이용하기 위해 초기 연산을 수행하는 방법이다.

조회 테이블은 특정 작업을 실행 할 때 반복적인 연산이 발생하는 것을 피하기 위한 것이다.

입력받는 값의 n승을 계산하여 반환하는 기능이 있다면

매번 실행될때마다 pow 과정을 실행하는 것이 아닌

int[] powerOfTwos = new int[100];

for (int i = 0; i < 100; i++)
	powerOfTwos[i] = (int)Math.Pow(i, 2);

처럼 미리 연산해둔 값을 저장하여 가지고 이후에는 해당하는 값에 조회만해서 가져오는 방식 

 

-메모화

특정 입력 값에 대한 함수의 처리 결과를 기억하는 과정

특정 함수에 입력 값을 전달할 때마다 프로그램이 결과를 기억해 두고 이후 입력 값이 같다면 함수를 다시 실행 하지 않고 저장된 곳에서 결과를 얻는다

Dictionary<int, int> memoizeDict = new Dictionary<int, int>();

if (memoizeDict.ContainsKey(number))
{
	return memoizeDict[number]
}
else
{
	int i = number * A()....
    	memoizeDict.Add(number, i)
    	return i;
}

처럼 A()함수의 처리 결과가 딕셔너리에 이미 저장되어 있다면 A() 함수 처리를 하지 않고
딕셔너리에서 바로 값을 반환 하는 형식이다.

 

이번 챕터를 공부하면서 느낀 점은

지연, 사전 연산, 메모화 등의 기법은

함수형 프로그래밍에 한해서 이용하면 더욱 더 효율적인 코드를 작성할 수 있다라기 보다는

우리가 전반적으로 코드를 작성하면서 이런 기법들을 잘 사용한다면 최적화된 효율적인 코드를 작성할 수 있다라는 생각을 많이 하게 되었고,

정식적인 기법 명칭을 인지하고 있지 않았을 뿐, 일부는 코드를 작성하면서 사용하고 있었던 것들도 있었다 

728x90
Posted by 정망스
,
728x90

재귀 호출은 for문 while문 같은 반복에 비해 더 짧게 구현 가능한 경우가 많기 때문에 함수형 접근 방식에 적합하다고 한다. 대신 설계 및 테스트는 어려운 편


재귀 호출은 기본 케이스를 갖는 것이 일반적이며 기본 케이스란 재귀의 종료 조건을 정의하는 것을 말한다. 


반복하는 형태의 구현이 더 효율적인 경우에 컴파일러가 재귀 호출을 반복으로 변환하기도 한다고 한다.


기본 케이스가 실행될 때까지 끊임없이 자신을 호출하는 형태직접 재귀 호출(꼬리 재귀, 누적기 전달형, 연속 전달형..)이라 하고, 

간접 재귀 호출이라는 것도 있는데 간접 재귀는 최소 두 개의 함수가 관여하는 형태로 함수 A,B가 존재할 경우 함수 A가 B를 호출하고, 함수 B가 다시 A를 호출하는 형태

이 형태를 재귀 호출로 볼수 있는 근거는 함수 B가 A를 호출할 때 함수 A는 이미 B를 호출하면서 활성화 되었기 때문. 즉 함수 B가 A를 호출했을 때, 함수 A의 호출이 아직 완료되지 않은 상태로 남아 있다.

bool IsOdd(int number)

{

if (number == 0) return false;

else return IsEven(number - 1);

}

bool IsEven(int number)

{

if (number == 0) return true;

else return IsOdd(number - 1);

}


LINQ의 Aggregate를 적용하면 재귀 함수를 함수형 접근 방식으로 리펙토링 할수 있다.

int[] numbers = { 1, 2, 3, 4, 5 };

var data = numbers.Aggregate((num1, num2) => num1 * num2);

결과 : 120


요약하면 재귀 함수를 사용하면, 표현이 어렵지 않게 누구나 보기에 자연? 스러워 진다면 가독성이 좋아지고, 변수의 사용을 줄여줌으로써 함수형 프로그래밍에 알맞다라고 하는거 같다.

함수를 일급 객체로 취급하는 함수형 프로그래밍에서 함수에서 함수를 호출하여 처리 하는 방식인 점도 있는거 같다.

하지만 재귀 함수도 잘못 사용 되어질 경우엔 메모리를 많이 차지할수도 있고 성능이 안좋을수도 있다 (스택에 매개변수, 지역변수, 리턴 값, 종료후 돌아갈 위치 등 스택 메모리에 쌓이니까.. 스택오버플로우)

그리고 가독성이 좋아진다라고 책에는 정리 되어 있지만, 복잡한 로직의 재귀 함수일 경우엔 오히려 이해하거나 보는데 있어서 가독성을 해칠 경우가 있을거 같다.

즉, 재귀 함수의 형태는 함수형 프로그래밍에 맞지만 이것을 이용하여 개발하는데 있어서는 주의할 점과, 고려해야할 것 등 어려움이 많을수 있다 정도 인거 같다.

728x90
Posted by 정망스
,
728x90

C# 3.0 에서 처음 소개된 언어 통합 질의 (LINQ, 링크) 는 IEnumerable<T> 인터페이스를 구현하는 컬렉션 등으로부터 필요한 데이터 원본을 쉽게 질의 할 수 있다.

LINQ는 컬렉션 데이터를 질의할 때 지연 실행 개념을 이용한다

var numbers = new List();
numbers.Add(1);

var r01 = numbers.Select(x => x * 10);

numbers.Add(2);

foreach (var x in r01)
{
	Console.WriteLine(x);
}

 

위 결과는 10, 2가 아니라 -> 10, 20이 나온다

이유는 질의를 완성하고 나서 바로 시퀀스를 반환하는것이 아니라 foreach문에서 열거 작업이 실행될때 쿼리가 실행되는 지연 실행 때문이다.

지연 실행을 원하지 않고 바로 반환을 할수도 있다.

Count(), First(), ToList(), ToArray() 와 같은 메서드를 사용하면 개체를 반환하는 메서드이기 때문에 바로 실행이 된다.

플루언트 구문 : 람다식 형태의 매개변수, 확장 메서드에 따른 메서드 체인으로 작성하는 코드 방식

쿼리식 구문 : SQL 질의와 비슷하게 작성하는 코드 방식

컴파일러는 컴파일 과정에 쿼리식을 플루언트 구문으로 변환한다

쿼리식으로 표현한 것은 플루언트 구문으로도 작성이 가능하다.

함수형 프로그래밍에선 플루언트 구문이 최적의 방법이라고 할 수 있다? ( 플루언트 구문이 람다식, 메서드 체인 등을 이용한 코드 방식때문에 코드 수를 줄일 수 있다는 장점에서 현재 .보는 책에서는 언급이 된 거 같다)

Enumerable 클래스는 System.Linq 네임스페이스에 속해 있으며 50여개의 표준 질의 연산자를 포함한다.

 

필터링 : 조건을 만족하는 것들을 추출하기 위해 데이터 요소를 평가하는 작업

(Where, Take, Skip, TakeWhile, SkipWhile, Distinct)가 있다.

 

투영 : 개체를 다른 형태로 변환 하는 것

(Select, SelectMany)가 있다

 

조인 : 직접적인 객체 모델 관계를 갖지 않는 서로 다른 원본 시퀀스들을 하나의 출력 시퀀스로 짜맞추는 작업

(Join, GroupJoin)가 있다

 

정렬 : 기본 비교자를 이용해서 결과 시퀀스를 정렬하는 작업

(OrderBy, ThenBy)가 있다

 

그룹화 : TKey 키 값에 따라 그룹화된 IGrouping<Tkey, Telement> 형식의 개체들로 구성된 시퀀스를 생성한다.

(GroupBy, Group, by)가 있다

 

집합연산 : 동일 컬렉션 혹은 별도의 컬렉션 내에서 일치하는 요소의 존재 여부에 기반한 결과 집합을 반환한다.

(Concat, Union, Intersect, Except)가 있다

 

변환 메서드 : 컬렉션의 형식을 변한하기 위한 것

(OfType, Cast, ToArray, ToList, ToDictionary, ToLookup)가 있다

 

요소 연산 : 인덱스나 조건자를 이용해서 시퀀스가 포함하는 개별 요소를 추출하는 작업

(First, FirstOrDefault, Last, Single, SingleOrDefault, ElementAt, DefaultIfEmpty)가 있다

 

 

1. LINQ의 장점은 데이터를 다룰 때 필요한 연산을 한 곳에 묶을수 있다는 것인거 같다.

하지만 LINQ의 표현식을 사용해서 작업하다 보면 때로는 남용될 가능성은 없을까?

그렇다면 오히려 가독성이나, 코드 정리를 해칠수도 있지 않을까 

이럴땐 오히려 간단한 메서드 호출이 더 짧고 가독성이 좋지 않을까 

 

2. LINQ의 지연 실행의 장점은 질의를 한번 정의하고 나면 필요할 때만 별도로 호출 할 수 있기 때문에 이 장점을 잘 이용하면 메모리 부하가 적고 자원 낭비가 덜한 코드를 작성할 수 있다는 것이다.

하지만 (Avarage, Count)등, 몇몇 연산자 (orderBy, Reverse)등과 같은 질의 연산자들은 원본 시퀀스를 반복하기 때문에

메모리 효율을 저하시킬수 있어서 이러한 점을 인지하고 사용하는게 좋다고 한다.

 

3. 성능에선 어떤게 좋을까?

StackOverFlow에서 보면 LINQ같은 경우엔 개발적인 속도와, 유지 보수성에선 LINQ에 손을 들어주는 대신

성능을 우선시 해야할 경우엔 수동으로 코드를 작성해주는 것이 더 좋다라는 식에 의견이 많은 것 같다.

모든지 과하면 안좋은 것 처럼, 무작정 쓰지 말아야 한다! 라는것 보단 적절한 선에서 적절히 사용한다면 좋은 프로그램 작성과, 효율을 다 가져갈 수 있지 않을까? ( 너무 뻔한말인가..)

 

728x90
Posted by 정망스
,
728x90

확장 메서드는 기존 클래스나 형식에 변경을 가하지 않고 기능을 확장할 수 있는 방법이다.


public static bool Extension(this string str) {...}

처럼 첫 번째 인수에 this 키워드가 있는데 이것이 확장 메서드임을 의미한다.

확장 메서드는 정적 클래스에서만 정의할 수 있다.


메서드 체인은 읽기 쉽고 짧은 코드를 작성할 수 있도록 해주기에 함수형 프로그래밍 작성에 어울리고, 메서드 체인은 확장 메서드에 의존하는 관계를 가진다.


A(), B() 함수를 이용한 string 값을 만드는 상황에서

확장메서드가 미적용일 경우엔

string result = "";

string strTemp = "ABCD";

strTemp = A(strTemp);

strTemp = B(strTemp);

result = strTemp;


확장메서드를 적용할 경우엔

string result = "";

string strTemp = "ABCD";

result = strTemp.A().B();

처럼 코드를 더 간결하게 만들수 있다 라는 장점이 있다.


기존에 정의되어 있는 메서드와 동일한 이름을 갖는 확장 메서드를 만들 경우 매개 변수 형식과 갯수까지 같다면 기존 메서드가 실행 되므로 주의 해야 하고,

기존 형식의 코드 변경 없이 개발자 임의로 만든 메서드를 대상 형식에 쉽게 추가를 할수 있지만 너무 과하게 사용한다면 혼란을 야기할 수도 있기 때문에 적당히 사용한다면 좋은 프로그래밍 작성에 도움이 될 듯 하다.



728x90
Posted by 정망스
,


맨 위로
홈으로 ▲위로 ▼아래로 ♥댓글쓰기 새로고침