본문 바로가기

Compute

FUNCTIONAL PROGRAMMING

이 글은 , “함수 프로그래밍 입문” 이란 책의 0장을 읽고 정리한 글입니다.
関数プログラミング実践入門 ──簡潔で、正しいコードを書くために

0. [입문] 함수 프로그래밍

 

함수 프로그래밍에서 얻을수 있는 개선

코드 양이 적어진다

같은 기능을 가진 프로그램을 구현한다고할때 , 적은 행수로도 끝낼수 있다.
이것은 함수프로그래밍에서 “수학”의 개념을 적용 했기 때문 (수학은 고도로 추상화된 분야)
코드 양이 적다는것은 메인터넌스성이 높다.

최적화하기 쉽다

최적화는 많은 프로그래머가 노력을 들이는 부분 , 또 많은 부분을 컴파일러에서 자동으로 해줌
컴파일러가 최적화를 위해 사용할수있는 유용한 특징을 가지고 있을수록 좋음.
함수프로그래밍의 추상화 능력이 컴파일러의 최적화에 도움을 준다.

병행/병렬화하기 쉽다

CPU의 진화의 방향은 기존 동작주파수를 높이는 것에서 코어를 늘리는 것으로 변화됨
CPU가 빨라지면 프로그램도 빨라지던 과거와는 달리 , 병행/병렬화된 프로그램이 아니면 새로운 CPU라도 빨라진 효과를 느끼기 어려워짐.
함수타입 프로그래밍은 병행/병렬화 하기 쉽기때문에 많은 효과를 얻을수 있다.

버그를 내기 어렵다

일부의 함수타입언어에서는 제약조건을 ‘타입’로 표현하고 , 그 제약조건을 지키고있는지 컴파일시간등에 조사하는것이 가능.
이러한 제약조건으로 메인터넌스,가독성이 높은 코드를 쓰는 것이 가능

문서화가 적어진다

위의 높은 제약조건이 컴파일시등에 검사되기 때문에 일부러 제약조건에 대해서 문서화를 하지않아도 됨
문서화 하지 않아도 되기때문에 코드를 작성할 시간을 확보할수 있다 .
또 문서를 통해 제약을 거는것보다는 코드를 통해 제약을 거는 편이 효율적

함수란 무엇인가?

//C++
// say.cpp
//문자열을 표준출력
void say(const std::string& something) {
     std::count << something << std::endl; // 외부와의 입출력을 행하고 있음
}

위의 코드는 함수타입 코드가 아님
외부에 문자를 출력하는 행위가 외부의 환경에 의존하기 때문에 항상 같은 결과가 나온다고 말할수 없기 때문.

# Ruby
# current.rb
# 현재시간을 취득
def current
 Time.now #부여된 입력값이외로부터 출력값이 정해짐
end

위의 코드에서 current는 외부로부터 시각을 취득.
current는 인수가 없기 때문에 입력으로부터 출력이 정해지는 함수적의 의미에서는 항상 같은 값이 된다고 말할수없음.
실제로 current를 실행할때마다 매번 다른 결과가 출력됨

//C++
// show.cpp
// 정수값n을 문자열로 교환한다
// show(1234) => “1234"
std::string show(const int n ) {
  std:ostringstream oss;
  oss <<n;
  return oss.str();
}

//Ruby total.rb
//numbers 의 값을 전부 더한다
total ([1,2,3]) => 6
def total(numbers) numbers.inject(:+) end

위와 같은 코드는 같은 값을 입력하면 항상 같은 값을 취득할수 있기때문에 함수타입 언어라고 말할수 있음.

부작용

프로그래밍에서 상태의 참조 또는 상태에 변화를 주는 것으로 이후의 결과까지 영향을 주는 효과를 부작용이라고 부름.

  • assign
    • 변수의 상태가 참조하는 타이밍에 따라 변하기 때문.
  • 입출력
    • 화일에 쓰기 또는 읽기
    • 물리디스크로부터 읽는 행위는 물리적 동작을 발생하기하고
    • 그 동작에따라 물리디스크가 파손되었을경우 , 다음의 읽기에 영향이 있음

부작용을 가진 프로세스는 함수가 아님.
함수 프로그래밍에 관해 있어서 대상으로하는 처리내용에 부작용이 있는지 없는지를 구분하는것은 중요.

함수 프로그래밍이란 무엇인가?

프로그래밍의 패러다임

명령타입 프로그래밍의 패러다임

  • 프로그래밍은 계산기가 행해야하는 명령의 열

오브젝트 지향 프로그래밍의 패러다임

  • 프로그램은 오브젝트와 메세징 , 프로그래밍은 문제에서 어떤 오브젝트가 있는지 분류해 정의
  • 오브젝트를 생성/관리하고 오브젝트끼리의 메세지를 주고받음으로 문제를 해결하고, 기능을 구현

함수프로그래밍의 패러다임

“프로그램이란 함수이다.”라고 보는 방법.
큰 프로그램은 작은 프로그램의 조합으로서 만들어진다.
(프로그램을 조합하는것은 함수합성(function composition)이라고 함.)

f(x), g(x) -> h(x) = f(g(x))

조합하기 쉬운 , 부품화하기 쉬운 독립성을 modularity라고 하고 함수와 함수합성이 가진 성질은 높은 modularity를 보장.

함수타입언어란것은?

함수타입언어의 조건

  • 함수가 제일급의 대상이 된다. (first class)

함수가 언어에서 단순한 값과 같이 아래와 같은 취급을 해야함.

일급대상이란?(first class)

  • 함수의 리터럴이 있음
    • 코드에 하드코딩된 숫자나 문자열처럼 , 함수의 표현이 존재
  • 함수를 실행시에 생성
    • 함수합성의 의해서 생성이 가능, 부분적용,고차적용 등에 의해서 작성됨
  • 변수에 assign이 가능
  • 절차와 함수에 인수로서 부여가 가능
  • 절차와 함수의 결과로서 돌려주는것이 가능

함수타입언어와 명령타입언어

명령타입언어

어떤 결과를 내기위해서 CPU또는 처리계의 낮은 레벨의 동작을 드러나도록 코드를 작성할 필요가 있다.

  • 값을 어딘가 저장할수있는가(assign)
  • 어디로부터 값을 가져올수 있는가(참조)
  • 다음으로 어떤 절차를 넘길수있는가(철자의 호출)

기본적으로 변수는 어떤 값을 저장하는 상자의 개념
이 상자에 값을 넣는 횟수에 어떤 제약이 있지는 않다.
한번 assign한 변수에 다른 값을 넣는 “파괴적 assign 조작”이 가능.

함수타입언어

함수타입언어에서는 assign자체가 없거나 , 한정되어 있어서 기본적으로 한번 변수의 값을 정하면 변경할수 없는 속박(binding)이 되어버림.

  • 값이 항상 들어있다
  • 값이 변경되지 않는다

assign이 없이 속박이 되어버림으로서 얻을수있는것은

  • null check같은 본질적이지 않은 처리를 할 필요가 없어진다.
  • 코드의 가독성증가
  • assign 코드의 최적화가 쉬워짐

함수타입언어의 특징적 기능

타입과 타입없음 (typed , untyped)

타입없음의 경우 값과, 계산결과를 특별히 구별하지 않기때문에 ,계산결과가 어떤타입이 될지는 예상할수가 없다.
전부 인간의 일이 되어버림. (이 부분을 type system으로 맡기느냐 아니냐의 차이)

정적타입 , 동적타입

컴파일타임에 타입을 붙이는가 , 실행시에 타입검사를 해서 붙이는가에따라 구별

순수
같은 식은 언제 평가를 하더라도 같은 결과가 되는 것 , 참조 투과성(referential transparency)이라 불림

타입검사

타입검사(type checking)란 타입의 정합성을 컴파일시등에 검사하는 기능. 검사에 실패할경우 에러를 표시
강한 타입, 약한 타입

안전성 – 언어사양에 정의되지 않은 동작을 일으키지않는것 (ex – c언어의 메모리침범)
타입검사후에 안정성을 확보할수 있는것을 강한 타입(strong typing) , 그렇지 않은 것을 (weak typing)
일반적으로 정적타입은 강한 타입인 경우가 많다.

타입추론

프로그래머가 부여한 타입의 정보로부터 부여되지않은 타입을 추론하는 기능.
(ex- 숫자와 문자열의 합성은 문자열로서 판단될수있음.)
의존타입(dependent type)

다른 타입에 의존한 타입 또는 값에 의존해 타입을 만드는 기능
(ex – Dictionary<String,Number>)

평가 전략

어떤순서로 식을 평가하는가,
적극평가와 지연평가가 존재.
적극평가(eager evaluation) -> 인수가 전달되는 때에 평가 (계산할수 있는것은 전부 미리 계산)
지연평가(lazy evaluation) -> 실제 필요할때까지 평가를 하지 않는 것

int tarai (int x , int y , int z) {
return (x <= y) ? y :
(tarai (tarai(x-1, y,z),
tarai(y-1,x,z),
tarai(z-1,x,y)));
}

적극평가의 경우 x가 y보다 작을경우에 필요하지 않은 z의 결과까지 계산하기때문에 느려짐.
이 경우는 특별한 경우로서, 대부분의 경우 적극평가의 경우가 빠른경우가 압도적으로 많다.

왜 지금에서 함수타입언어인가?

추상화

보통 추상화라고 한다면

  • 많은문제에 대해서 범용적인 적용이 가능
  • 추상화후의 세계에 조작이 풍부하다

함수타입 언어로 추상화를 진행할때 명확하게 좋다고 느끼는 부분은 집합론(群論,group theory),권론(圏論,category theory) 라고하는 수학 쪽의 추상화

수학의 역사는 컴퓨터의 역사보다도 긴 동안에 추상화의 과정을 거쳐왔음.
때문에 수학의 수학의 성과를 그대로 적용할수 있다면 보다 높은 추상화를 얻을수있다.

함수타입언어의 최적화

언어에서 최적화는 중요한 요소, 컴파일러에게 맡길수 있는 최적화의 여지가 클수록 개발자가 편해질가능성이 높음.

//C (c99)
int totalFrom1To(int n) {
int result = 0;
for (int i = 1 ; i <= n; result += i++);
return result;
}

1부터 n까지 더하는 함수

— Haskell
totalFrom1To = sum.enumFromTo 1
(enumFromTo 1 리스트를 만들어서  sum을 계산함)

일반적으로 c언어가 더 빠르지만,
구조를 만드는 함수와, 구조의 접는(합치는) 함수를 합성할 경우 중간구조를 만들 필요가 없다는 수학의 성과가 존재.
를 이용해서 아래와 같은 C언와 같은 수준의 최적화가 가능.

— Haskell
totalFrom1To = auxTotalFrom1To 0 where
auxTotalFrom1To result 0 = result
auxTotalFrom1To result n = auxTotalFrom1To(result + n)(n-1)

[1부터 n까지 리스트를 작성]
[작성한 리스트의 값을 전부 더함]

의 코드보다는

[result에 더해지는 수i를 1부터 증가시켜 n까지 더함]

쪽이 부품화되어있음. 또 동작및 확인이 간단해짐 (보충하면 프로그램의 정확성측면에서 두개의 기능을 가진 부품 보단 ,하나의 기능을 가진 부품으로서의 결과가 더 정확하다는 말)

함수타입 언어가 수학적으로 추상화하기가 쉽기 때문에 ,다른 언어의 일반적 최적화와 더불어 수학적 추상화를 이용해 최적화 단계를 적용하는 경우가 있다.

명령타입 언어쪽이 cpu에 보다 가까운 추상화도가 낮은 위치에 있기때문에 , 단순히 명령타입언어와 비교할경우 실행속도로 함수 타입 언어에 불리한 점이 있는것이 사실

  • 명령타입 언어에서 최적화를 넣어도 원하는 성능이 나오지 않을경우 , 더 이상 튜닝하기가 어렵지만,
  • 함수타입언어는 추상도가 높기 때문에 연구에 의해 최적화를 더할 여지가 크다고 말할 수 있다.

함수타입언어와 병행/병렬 프로그래밍

매년 cpu의 코어는 증가 , 주파수의 증가는 더이상 기대하기가 힘들다.

병행(concurrent)/병렬(parallel)의 개념과 프로그래밍의 어려움

  • 전 프로그램안에서 동작하는 병행 실행의 코드를 작성하는 경우 , 각각의 처리가 뒤섞여서 비대해지는 경우가 많다.
  • 재현성이 낮은 버그가 나오는 경우도 많다.

병행(concurrent)프로그래밍은 복수의 task가 동시에 실행될 필요가 있을때 유용.

  • 병렬(parallel)프로그래밍은 각 코어에서 실행하더라도 같은 결과가 나올수 있는경우에 유용. (단일task의 고속화)

병행 프로그래밍의 어려움

병행프로그래밍 = multithreaded Programming

문제점 -> 복수의 thread가 동일한 resource를 같은 시간에 엑세스 할 경우

  • resource가 기대하지 않은 상태가 되어있는 것 (race condition)
  • 보통 mutex라 불리는 배타제어를 할 필요가 있음.

하지만 이 lock를 이용한 제어에는 문제가 많아서

  • lock을 해야할 장소(사양,설계,코드)에 lock을 잊어버렸다.
  • lock한뒤에 unlock을 잊어버렸다.
  • lock할 필요가 없는/또는 있었지만 필요없어진 리소스에 대해 lock을 해서 퍼포먼스가 낮음
  • lock을 취득한 범위가 쓸때없이 크기때문에 퍼포먼스가 낮음
  • 스레드 2개가 서로 lock한 리소스를 기다리고 있음(deadlock)

deadlock의 조건

  1. lock을 해야할 2개 이상의 리소스가 이미 존재
  2. 1개이상의 리소스를 lock한채로 별도의 리소스의 lock을 요구하고 기다리고 있다
  3. 이미 lock된 리소스를 취득 할 수가 없다.
  4. 리소스의 lock을 취득하는 순서가정해져있지 않다.

멀티스레드 프로그래밍에선 첫 3개의 조건을 깨는 것은 정말 어렵다

  • 일반적으로 최후의 조건을 제거하기 위해 , 리소스의 lock순서를 정해서 대처.
  • 위의 방법은 인간의 주의력에 의존하는 방법으로 , 전체 프로그램안에서 리소스와 lock의 순서,
  • 그리고 그것을 문서화하지 않으면 관리가 어려움.

 

병렬 프로그래밍의 도움 – 참조투명성의 보증

함수타입 언어의 특징

  • 파괴적 assign 및 조작이 불가능
  • 순수함수타입언어에선 언제 함수를 평가하더라도 같은 결과 -> 병렬화 하기가 쉽다
    • (다른 언어에서도 참조투명성을 보장한다면 병렬화해도 문제는 없다.단지 주의력의 문제)

틀리지않고 작성할수 있다면 , 그리고 틀리지않고 메인터넌스가 가능하다면
현대의 계산기 아키텍쳐에서는 명령타입 언어쪽이 빠른경우가 많다.
하지만 계속 업데이트를 해야하고 메인터넌스를 진행해야하는 프로그램의 경우 ,
병렬처리에 대한 언어적 지원이 있다고한다면 개발자에게 도움이 됨.

함수타입 언어의 미래

명령타입언어의 진화

  • 구조화프로그래밍의 도입 (1968년 dijkstra의 goto의 위험성에 대한 논문)
  • 캡슐화이 도입 (객체지향에 의해 내부의 상태를 숨김)

언어의 진화를 보면 , 강한 제약을 주는 것으로 편리하고 안전한 어프로치가 가능해졌음.

이 제약은 두가지로

  • 언어기능으로서 주어진 제약 (let , var 등의 참조투명성을 지켜야하는 언어적제약)
  • 프로그래머가 줄수있는 제약 (const , interface , protocol등의 개발자가 걸어둔 제약)

최신의 언어는 점점 언어적 제약을 늘려가는 추세

함수타입 언어를 채용하는 메리트

  • 언어적지원의메리트
    • 본질적이지 않은 사소한 것으로부터의 해방 (ex – 평균함수)
  • 제약의 충족을 체크해주는 메리트
    • 참조투명성같은 언어적제약
  • 타입검사가 있어서 생기는 메리트
  • 타입추론의 메리트