lionhairdino

@lionhairdino@hackers.pub · 45 following · 51 followers

지금까지 다루어 봤던 언어는 아래와 같습니다. MSX Basic Z80 Assembly Pascal GW-Basic C Macromedia Director Visual Basic PHP Flash Actionscript C++ Javascript

그리고 지금은, 하스켈을 비즈니스에 쓰려고 몇 년간 노력하고 있습니다. 지금 상태는, 하스켈 자체를 연구하는 게 아니라, 하스켈 (혹은 함수형 언어) 이해가 어려운 이유를 연구하는 아마추어 연구가쯤 되어버렸습니다. 하스켈 주제로 블로그를 운영 중이지만, 아직은 하스켈 프로그래머라고 자신 있게 말하진 못하고 있습니다. 가끔 이해에 도움이 될만한 측면이 보이면, 가볍게 아이디어를 여러 SNS에 올려보곤 하는데, 그다지 프로그래머에게 쓸모 있는 내용이 포함되진 않는 것 같습니다.

Blog
lionhairdino.github.io

업자를 위한 아주 인포멀한 모나드 설명

lionhairdino @lionhairdino@hackers.pub

1.

함수형에선, 스트림 [1,2,3]
(+1)map해서 [2,3,4]를 만들고,
(+2)map해서 [3,4,5]를 만드는 작업을,
(+2) ∘ (+1)[1,2,3]map하는 걸로 표현할 수 있어야 한다.

(+1), (+2), ((+2) ∘ (+1)) 함수들은 모두 Int -> Int 함수를 원하는 곳에 넣어 줄 수 있는 함수들이다.

위와 같이, 완벽하게 정보를 유지하진 않지만, 같은 "류"의 작업을 두 번 하는 것을, 한 번 작업하는 것으로 표현할 수 있는 경우도 있다. 예를 들어, 첫 번째 작업으로, "hello"를 로그로 남기고, 두 번째 작업으로, " world"를 로그로 남기는데, 이를 한 번의 작업으로, "hello world"를 로그로 남기는 작업으로 표현할 수 있다. 여기는 로그를 남기는 횟수 정보는 필요 없고, 최종 로그만 필요하다는 인위적 정보 선택이 들어가 있다. 이 인위적 선택(여기선 로그 문자열을 합치는 것)을 수긍해야만 가능하다.

로그를 남기는 작업을 m이라 부를 때, m a를 받는 곳에 m (m a)를 넘길 방법이 생긴다는 뜻이다. 달리 말하면, m (m a)로 표현되는 작업을 인위적인 절차를 거쳐 m a로 만들어도, 내가 필요한 정보는 사라지지 않는다는 뜻이다.

2.

무언가가 하나인데, 유심히 보면 하나가 아닌 경우, 이게 바로 모노이드다. mono는 하나를 뜻하고, ~oid는 "척"하는 걸 말한다. (예. 인간인 척 하는 휴머노이드) 하나인척 하는 게 모노이드다. 수학 책 앞 부분에서 이항 연산, 결합 법칙, 항등원이 있으면 모노이드라는 설명을 하는데, 그래서 모노이드가 뭐에 쓰는 물건인지는 한참 공부해야 알 게 된다.

(아래는 혼자만의 생각입니다.)
모노이드를 바라 보는 눈 중 하나로, "모든 대상을 이항 연산으로 표현"을 들 수 있다.

0을 포함한 자연수들 0,1,2,3,... 들은, + 이항 연산과, 이 연산의 항등원 0이 있으면, 모두 ○ + ○ 한 가지 모양으로 표현할 수 있게 된다.
0 -> 0+0
1 -> 0+1
2 -> 0+(1+1) = 1+1
...
모노이드 구조이기에, 어딘가에서 ○ + ○ 모양을 원한다면, 0,1,2,3,...을 모두 넣어 줄 수 있다.

3.

"어딘가에서 m a를 원한다면, m a, m (m a), m (m (m a)), ...를 모두 넣어 줄 수 있다."를 위와 비교하며 보자.

위에서 얘기한 인위적 선택 작업join으로 표현하면,
m (m a) --join--> m a
m (m (m a)) --join--> m (m a) --join--> m a
...
m 반복 작업을 모두 ○ --join--> ○ 모양으로 표현할 수 있을 것만 같다. 그런데, 딱 하나는 표현하지 못한다. joinm이 두 개 있는 걸, 하나로 만드는 작업이라, m하나를 ○ --join--> ○로 표현하지 못한다. mjoin이 들어간 모양으로 표현하려면, 자연수, + 에서 처럼 0에 대응하는 것이 필요하다. m하나를, m 두 개로 만들되, 최종 결과에 영향을 미치지 않는 pure라는 작업을 만든다. 위 로그 작업을 예로 들면, 로그로 빈문자 ""을 추가하는 작업을 pure로 만든다. 그러면 이제야 비로소, 모든 반복된 m 을 join으로 표현할 수 있게 된다

m a --pure--> m (m a) --join--> m a
m (m a) --join--> m a
m (m (m a)) --join--> m (m a) --join--> m a
...

이제, join절차가 항상 있는 m a를 원하는 곳에 m am (m (m a))도 넣어 줄 수 있게 되었다. "hello"와 " world"를 남기던 두 개의 작업 합쳐, "hello world"를 남기는 하나의 작업으로 표현할 수 있게 되었다.

※ 지금 눈에 명확히 보이진 않지만, m 둘을 합성하는 연산을 .이라 하면, .만으론 모노이드 이항 연산 역할을 못하지만, join의 도움을 받고, id 만으론 항등원 역할을 못하지만, pure의 도움을 받아 모노이드 구조를 이룬다.

결론.

당연히 모든 내용이 담겨 있진 않고, 모나드를 무엇의 모노이드로 보는 내용을 비수학적으로 풀어 봤다. 모노이드는 모두를 하나의 모양으로 표현 할 수 있다는 걸, 보증해주는 거대한 개념이지만, 업자인 나에겐 "그렇게 해도 된다"는 정도의 느낌만 있다. (결합 법칙이 빠졌는데, 나중에 코드를 모듈화 하는 것과 연관지어 보면, 명확한 대응을 알 수 있다.)

모나드는, 조금 다르게 생긴 것을, 당장 필요한 요소만 잘 관리한다면 "같은 걸로 치자"를 멋지게(,어렵게) 형식화한 이론이다.

사족.
저와 대화를 나눠본 분들은 아시겠지만, 제가 비전공자라 용어 선택이나 개념 정의가 매우 인포멀해서 인상을 찌푸리는 경우도 자주 만듭니다. PL 전공자분들처럼 깊숙히 이론을 파고 싶은 게 아니라, 현실에 적용할 수 있을 만큼의 눈만 가지고 싶습니다. 현실을 모델링할 때, "인위적 정보 선택"을 해서 필요한 정보를 남길 수 있는 경우를 알아채는 눈을 길러야 되는데, bind 또는 flatmap, return 또는 pure가 있는 구조가 모나드라고만 배우면, 이런 눈을 가지는데 매우 오래 걸리는 것 같습니다.

비전공 업자분이 보셨다면, 얻어 가시는 아이디어가 있었으면 좋겠고, 전공자분이 보셨다면, 인포멀한 부분에 너무 인상 찌푸리지 마시고, 틀린 개념이 있다면, 부드럽게 조언을 해주시면 좋겠습니다.

※ 모나드 용어는 monotriad에 온 게 아닐까 의심한다는 설이 있습니다.(검색해 보면 근거는 미약해 보입니다.) 모나드는 join, return 그리고 위에서 명시적 언급은 안했지만, 펑터의 fmap, 이렇게 세 개 triad의 도움을 받아 모노이드로 만들 수 있는 구조입니다.

※ "정교한" 내용이 아님을 강조하고 선입견이 생기지 않기 위해, 일부러 제목을 달지 않고, 반말(혼잣말)투로 썼습니다.

제목은

  1. 함수형
  2. 모노이드
  3. 모나드

순서 입니다.

Read more →
6

mkDerivation 인자로 함수를 넘기는 이유

lionhairdino @lionhairdino@hackers.pub

NixOS.kr 디스코드에 올렸는데, 다른 분들의 의견을 들어보려고 해커스펍에도 올립니다. 닉스 경험이 많지 않은 사람의 글이니, 정확하지 않을 수 있습니다. 틀린 곳이 있으면 바로 알려주세요.

※ 닉스를 처음 접하는 분들이나, 닉스로 실용적인 업무를 하시는데 문제가 없는 분들한테는 적당한 글은 아닙니다. 서두에 읽으면 도움이 될만한 분들을 추려서 알려 드리려고 하니, 의외로 필요한 분들이 없겠습니다. 실무자들은 이렇게까지 파고들며 알 필요 없고, 닉스 개발자들이야 당연히 아는 내용일테고, 입문자 분들도 역시 이런 내용으로 어렵다는 인식을 갖고 시작할 필요는 없으니까요. 그럼 남은 건 하나네요. mkDerivation 동작을 이미 아는 분들의 킬링 타임용이 되겠습니다.


외국어를 익힐 때, 문법없이 실전과 부딪히며 배우는 방법이 더 좋기는 한데, 가끔은 문법을 따로 보기 전엔 넘기 힘든 것들이 있습니다. 닉스란 외국어를 익히는데도 실제 설정 파일을 많이 보는 것이 우선이지만, 가끔 "문법"을 짚고 넘어가면 도움이 되는 것들이 있습니다.

닉스 알약 (제목이 재밌네요. 알약) 글을 보면 mkDerivation속성 집합을 받아, 거기에 stdenv 등 기본적인 것을 추가한 속성 집합을 만들어 derivaition 함수에 넘기는 간단한 래핑 함수임을 직관적으로 잘 설명하고 있습니다.

그런데, 실제 사용 예시들을 만나면, mkDerivation에 속성 집합을 넘기지 않고, attr: {...} 형태의 함수를 넘기는 경우를 더 자주 만납니다. 그래서, 왜 그러는지 실제 구현 코드를 보고 이유를 찾아 봤습니다.

  mkDerivation =
    fnOrAttrs:
    if builtins.isFunction fnOrAttrs then
      makeDerivationExtensible fnOrAttrs
    else
      makeDerivationExtensibleConst fnOrAttrs;

mkDerivation의 정의를 보면 인자로 함수를 받았느냐 아니냐에 따른 동작을 분기합니다. 단순히, stdenv에서 가져온 속성들을 추가한다면, 함수를 인자로 받지 않아도 속성 집합을 병합해주는 //의 동작만 있어도 충분합니다.

{ a = 1; } // { b = stdenv.XXX; }

하지만, 함수로 받는 이유를 찾으면, 코드가 단순하지 않습니다. 아래는 함수를 받을 때 동작하는 실제 구현 일부를 가져 왔습니다.

makeDerivationExtensible =
    rattrs:
    let
      args = rattrs (args // { inherit finalPackage overrideAttrs; });
      ...

전체를 보기 전에 일단 args에서부터 머리가 좀 복잡해집니다. argsargs를 재귀 참조하고 있습니다. 보통 rattrs 매개 변수로는 아래와 같은 함수들이 들어 옵니다.

stdenv.mkDerivation (finalAttrs: {
  pname = "timed";
  version = "3.6.23";
  ...
    tag = finalAttrs.version;
}

(와, 해커스펍은 코드 블록에 ANSI가 먹습니다! 지원하는 곳들이 드문데요.)
코드를 바로 분석하기 전에, 닉스의 재귀 동작을 먼저 보면 좋습니다.

재귀 생각 스트레칭

nix-repl> let r = {a = 1; b = a;}; in r
error: undefined variable 'a'

위 동작은 오류지만, 아래처럼 rec를 넣어주면 가능합니다.

nix-repl> let r = rec {a = 1; b = a;}; in r 
{ a = 1; b = 1; }

rec동작은 Lazy 언어의 fix로 재귀를 구현하는 동작입니다. ※ 참고 Fix 함수

rec를 써서 속성 안에 정의 중인 속성에 접근할 수 있습니다. 그런데, 아래 같이 속성을 r.a 로 접근하면, rec 없이도 가능해집니다.

nix-repl> let r = {a = 1; b = r.a;}; in r
{ a = 1; b = 1; }

닉스 언어의 Lazy한 특성 때문에 가능합니다.

이제, 원래 문제와 비슷한 모양으로 넘어가 보겠습니다. 아래같은 형태로 바로 자기 자신에 재귀적으로 접근하면 무한 재귀 에러가 나지만,

nix-repl> let x = x + 1; in x
       error: infinite recursion encountered

아래처럼 람다 함수에 인자로 넘기면 얘기가 달라집니다.

nix-repl> let args = (attr: {c = 1; b = attr.a + attr.c + 1;}) (args // { a = 1; }); in args
{ b = 3; c = 1; }

여기서 속성b의 정의 동작이 중요합니다. 위 내용을 nix-instantiate로 평가하면,

 nix-instantiate --eval -E 'let args = (attr: {c = 1; b = attr.a + attr.c + 1;}) (args // { a = 1; }); in args'
{ b = <CODE>; c = 1; }

--eval 옵션은 WHNF까지만 평가합니다. <CODE>는 하스켈 문서에서 나오는 <thunk>와 같은, 평가가 아직 이루어지지 않은 상태를 뜻합니다. ※ Nix Evaluation Performance
b는 아직 평가가 되지 않았으니, 에러가 나지 않고, b가 필요한 순간에는 attr.a가 뭔지 알 수 있는 때가 됩니다.

attr: 
  { c = 1; 
    b = attr.a + attr.c + 1;
  }

속성 b아직 알 수 없는, 미래의 attr에 있는 a를 받아 써먹고, 원래는 rec없이 접근하지 못했던, c에도 attr.c로 접근이 가능합니다. (attr.c은 현재 정의하고 있는 속성셋의 c가 아니라, 매개 변수 attr로 들어온 속성셋의 c입니다.)

원래 문제로 다시 설명하면, mkDerivation에 넘기고 있는, 사용자 함수 finalAttrs: { ... }에서, 닉스 시스템이 넣어주는 stdenv 값 같은 것들과 사용자 함수내의 속성들을 섞어서 속성 정의를 할 수 있다는 얘기입니다. 아래처럼 말입니다.

    tag = finalAttrs.version;

뭐하러, 이런 복잡한 개념을 쓰는가 하면, 단순히 속성 추가가 아니라, 기존 속성이 앞으로 추가 될 속성을 기반으로 정의되어야 할 때는 이렇게 해야만 합니다. 함수형 언어에선 자주 보이는, 미래값을 가져다 쓰는 재귀 패턴인데, 저는 아직 그리 익숙하진 않습니다.

Read more →
0

닉스의 derivation 생성식과 derivation의 차이

lionhairdino @lionhairdino@hackers.pub

이 글은 Nix를 처음 접하는 사람들을 위해 `derivation` 개념을 명확히 하고자 합니다. Nix에서 `derivation`은 패키지 빌드를 위한 명세서로 간단히 설명되지만, 실제로는 `derivation 생성식`과 `derivation 객체`를 구별하는 것이 중요합니다. Nixpkgs는 `derivation` 자체가 아닌 `derivation 생성식(.nix)`들의 모음이며, 이 생성식을 평가해야 `derivation`이 만들어집니다. `.drv` 파일은 평가 결과를 캐싱한 파일입니다. 저자는 Nix가 의존성들을 `derivation 생성식`들의 관계로 표현하고, 최종적으로 하나의 생성식이 하나의 패키지를 표현한다는 점을 강조합니다. 흔히 `derivation`이라 부르는 것은 런타임에 메모리에 올라온 `derivation 데이터 타입` 객체이며, 이는 `derivation 생성식`을 평가한 결과입니다. 이러한 구분을 통해 Nix 코드를 함수형 관점에서 더 명확하게 이해할 수 있다고 주장합니다. Nix의 `derivation` 개념을 더 깊이 이해하고 싶다면 이 글이 좋은 출발점이 될 것입니다.

Read more →
0

함수형 프로그래머한테 닉스 패키지 매니저의 derivation 소개하는 글

lionhairdino @lionhairdino@hackers.pub

이 글은 닉스의 핵심 개념인 derivation에 대해 설명합니다. Derivation은 패키지 빌드에 필요한 속성들의 집합으로, 닉스는 이를 통해 의존 관계를 순수하게 표현합니다. 패키지 A가 B에 의존한다면, A의 derivation이 B의 derivation에 의존하는 방식으로 명세서를 작성하고, 실제 패키지가 필요할 때 realize 동작을 통해 이펙트들의 영향을 받습니다. 이러한 선언적인 명세서 덕분에 닉스는 선언형 패키지 매니저라고 불립니다. Derivation 이름에 내용 기반 해시를 붙여 캐싱과 재현성을 높이지만, 작은 변화에도 해시값이 달라져 디스크 용량과 빌드 시간이 늘어나는 단점도 있습니다. Nixpkgs는 derivation 자체가 아닌, derivation을 생성하는 표현식들의 모음으로 구성됩니다.

Read more →
0