Nix 언어 기초#
- Author:
Valentin Gagarin @fricklerhandwerk
- Editor:
Silvan Mosberger @infinisil
Nix 언어는 derivation, 즉 기존 파일의 내용을 사용해 새 파일을 도출하는 방법을 정확하게 기술한 것을 편리하게 만들고 조합하도록 설계되었습니다. Nix 언어는 도메인 특화, 순수 함수형, 지연 평가, 동적 타입 프로그래밍 언어입니다.
Nix 언어의 주요 활용 사례
매우 복잡해 보이는 Nix 언어 표현식을 금방 마주칠 수 있습니다. 다른 모든 프로그래밍 언어와 마찬가지로, 필요한 Nix 언어 코드의 양은 해결하려는 문제의 복잡도와 밀접하게 일치하며, 그 문제와 해법을 얼마나 잘 이해하고 있는지를 반영합니다. 소프트웨어 빌드는 복잡한 작업이며, Nix는 Nix 언어를 통해 이 복잡성을 드러내는 동시에 관리할 수 있게 해 줍니다.
그러나 Nix 언어 자체에는 이 튜토리얼에서 소개할 몇 가지 기본 개념만 있으며, 이 개념들은 자유롭게 조합할 수 있습니다. 복잡해 보이는 부분은 언어 자체가 아니라 언어를 사용하는 방식에서 비롯됩니다.
개요#
이 문서는 다른 튜토리얼과 예제를 따라갈 수 있도록 Nix 언어 읽는 법을 소개합니다.
실제로 Nix 언어를 사용하는 것에는 여러 가지가 포함됩니다:
언어: 구문과 의미론
라이브러리:
builtins와pkgs.lib개발자 도구: 테스트, 디버깅, 린팅, 포매팅 등
범용 빌드 메커니즘:
stdenv.mkDerivation, 빌드 헬퍼 등조합 및 설정 메커니즘:
override,overrideAttrs, 오버레이,callPackage등생태계별 패키징 메커니즘:
buildGoModule,buildPythonApplication등NixOS 모듈 시스템:
config,option등
이 튜토리얼은 가장 중요한 언어 기능만 다루고 라이브러리는 간략히 설명하며, 마지막에는 나머지 구성 요소에 대한 참조 자료와 리소스를 안내합니다.
무엇을 배우게 됩니까?#
이 튜토리얼을 마치면 일반적인 Nix 언어 코드를 읽고 그 구조를 이해할 수 있습니다. 이 튜토리얼의 목표는 Nix 언어가 익숙한 언어와 어떤 점에서 다를 수 있는지 짚어 보는 것입니다.
따라서 Nix 언어에서 가장 흔하고 특징적인 패턴을 보여 줍니다:
함수 선언과 호출
빌드 입력을 얻기 위한 비순수성
빌드 작업을 기술하는 derivation
중요
이 튜토리얼은 Nix 언어의 모든 기능을 자세히 설명하지 않으며, 구문 규칙의 세부 사항도 다루지 않습니다. 예를 들어, 이 튜토리얼은 if ... then ... else ...와 같은 흔한 구문은 건너뜁니다.
전체 언어 참조는 Nix 매뉴얼을 참고하십시오.
무엇이 필요합니까?#
소프트웨어 개발에 대한 이해
명령줄 예제를 읽을 수 있도록 Unix 셸에 익숙해야 합니다
예제를 실행하기 위한 Nix 설치
얼마나 걸립니까?#
함수형 프로그래밍 경험이 없는 경우: 2시간
함수형 프로그래밍에 익숙한 경우: 1시간
함수형 프로그래밍에 능숙한 경우: 30분
모든 예제를 실행해 보십시오. 예제를 직접 바꿔 보면서 여러분의 가정을 검증하고 배운 내용을 시험해 보십시오. 예제를 완전히 이해했는지 확인하고 싶다면 자세한 설명을 읽어 보십시오.
예제는 어떻게 실행합니까?#
Nix 언어 코드 조각을 Nix 표현식이라고 합니다.
Nix 표현식을 평가하면 Nix 값이 만들어집니다.
Nix 파일(파일 확장자
.nix)의 내용은 Nix 표현식입니다.
참고
평가한다는 것은 언어 규칙에 따라 표현식을 값으로 변환한다는 뜻입니다.
이 튜토리얼에는 Nix 표현식 예제가 많이 들어 있습니다. 각 예제 뒤에는 예상되는 평가 결과가 나옵니다.
다음 예제는 두 수를 더하는 Nix 표현식입니다.
1 + 2
3
대화형 평가#
Nix 표현식을 대화형으로(명령줄에 입력하여) 평가하려면 nix repl을 사용하십시오.
$ nix repl
Welcome to Nix 2.13.3. Type :? for help.
nix-repl> 1 + 2
3
참고
Nix 언어는 지연 평가를 사용하며, nix repl은 기본적으로 필요할 때만 값을 계산합니다.
일부 예제는 이해를 돕기 위해 완전히 평가된 데이터 구조를 보여 줍니다. 출력이 예제와 일치하지 않으면 입력 표현식 앞에 :p를 붙여 보십시오.
예시:
nix-repl> { a.b.c = 1; }
{ a = { ... }; }
nix-repl> :p { a.b.c = 1; }
{ a = { b = { c = 1; }; }; }
nix repl을 종료하려면 :q를 입력하십시오.
Nix 파일 평가하기#
Nix 파일 안의 표현식을 평가하려면 nix-instantiate --eval을 사용하십시오.
$ echo 1 + 2 > file.nix
$ nix-instantiate --eval file.nix
3
자세한 설명
첫 번째 명령은 현재 디렉터리의 file.nix 파일에 1 + 2를 씁니다. 이제 file.nix의 내용은 1 + 2이며, 다음 명령으로 확인할 수 있습니다.
$ cat file.nix
1 + 2
두 번째 명령은 file.nix를 대상으로 --eval 옵션을 붙여 nix-instantiate를 실행합니다. 이 명령은 파일을 읽고 그 안에 담긴 Nix 표현식을 평가합니다. 결과 값이 출력됩니다.
다른 작업 없이 파일을 평가하기만 하려면 --eval이 필요합니다. --eval을 생략하면 nix-instantiate는 주어진 파일의 표현식이 derivation이라는 특수한 값으로 평가되기를 기대합니다. derivation은 이 튜토리얼 마지막 부분의 Derivation에서 다룹니다.
참고
파일 이름을 지정하지 않으면 nix-instantiate --eval은 default.nix에서 읽으려고 시도합니다.
$ echo 1 + 2 > default.nix
$ nix-instantiate --eval
3
참고
Nix 언어는 지연 평가를 사용하며, nix-instantiate는 기본적으로 필요할 때만 값을 계산합니다.
일부 예제는 이해를 돕기 위해 완전히 평가된 데이터 구조를 보여 줍니다. 출력이 예제와 일치하지 않으면 nix-instantiate에 --strict 옵션을 추가해 보십시오.
예시:
$ echo "{ a.b.c = 1; }" > file.nix
$ nix-instantiate --eval file.nix
{ a = <CODE>; }
$ echo "{ a.b.c = 1; }" > file.nix
$ nix-instantiate --eval --strict file.nix
{ a = { b = { c = 1; }; }; }
공백에 관한 참고 사항#
공백은 필요한 곳에서 어휘 토큰을 구분하는 데 사용됩니다. 그 외에는 아무 의미가 없습니다.
줄 바꿈, 들여쓰기, 추가 공백은 읽는 사람의 편의를 위한 것입니다.
다음은 모두 동일합니다.
let
x = 1;
y = 2;
in x + y
3
let x=1;y=2;in x+y
3
이름과 값#
Nix 언어의 값은 원시 데이터 타입, 리스트, 속성 집합, 함수일 수 있습니다.
원시 데이터 타입과 리스트의 예시는 속성 집합의 맥락에서 나옵니다. 이 절의 뒷부분에서는 문자열의 특수한 기능인 문자열 보간, 파일 시스템 경로, 들여쓰기된 문자열을 접하게 됩니다. 함수는 별도로 다룹니다.
속성 집합과 let 표현식은 값에 이름을 할당하는 데 사용됩니다. 할당은 등호 하나(=)로 나타냅니다.
Nix 언어 코드에서 등호(=)를 만날 때마다 다음을 기억하십시오.
왼쪽에는 할당되는 이름이 있습니다.
오른쪽에는 값이 있으며, 값은 세미콜론(
;)으로 끝을 구분합니다.
속성 집합 { ... }#
속성 집합은 이름-값 쌍의 모음이며, 이름은 고유해야 합니다.
다음 예제는 모든 원시 데이터 타입, 리스트, 속성 집합을 보여 줍니다.
참고
JSON에 익숙하다면 Nix 언어를 함수가 있는 JSON이라고 생각하십시오.
함수를 제외한 Nix 언어 데이터 타입은 JSON의 대응 타입과 똑같이 동작하며 생김새도 매우 비슷합니다.
Nix
1{
2 string = "hello";
3 integer = 1;
4 float = 3.141;
5 bool = true;
6 null = null;
7 list = [ 1 "two" false ];
8 attribute-set = {
9 a = "hello";
10 b = 2;
11 c = 2.718;
12 d = false;
13 }; # comments are supported
14}
JSON
{
"string": "hello",
"integer": 1,
"float": 3.141,
"bool": true,
"null": null,
"list": [1, "two", false],
"object": {
"a": "hello",
"b": 1,
"c": 2.718,
"d": false
}
}
재귀 속성 집합 rec { ... }#
앞에 rec을 붙여 선언한 속성 집합을 때때로 보게 됩니다. 이렇게 하면 집합 안에서 속성에 접근할 수 있습니다.
예시:
rec {
one = 1;
two = one + 1;
three = two + 1;
}
{ one = 1; three = 3; two = 2; }
참고
속성 집합의 원소는 어떤 순서로든 선언할 수 있으며, 평가 시에 정렬됩니다.
반례:
{
one = 1;
two = one + 1;
three = two + 1;
}
error: undefined variable 'one'
at «string»:3:9:
2| one = 1;
3| two = one + 1;
| ^
4| three = two + 1;
let ... in ...#
“let 표현식” 또는 “let 바인딩”이라고도 합니다
let 표현식을 사용하면 값에 이름을 할당하여 반복해서 사용할 수 있습니다.
예시:
let
a = 1;
in
a + a
2
자세한 설명
할당은 let과 in 키워드 사이에 둡니다. 이 예시에서는 a = 1을 할당합니다.
in 뒤에는 할당이 유효한 표현식, 즉 할당된 이름을 사용할 수 있는 표현식이 옵니다. 이 예시에서 표현식은 a + a이며, 여기서 a는 a = 1을 가리킵니다.
이름을 할당된 값으로 바꾸면 a + a는 2로 평가됩니다.
이름은 어떤 순서로든 할당할 수 있으며, 할당(=)의 오른쪽에 있는 표현식은 할당된 다른 이름을 참조할 수 있습니다.
예시:
let
b = a + 1;
a = 1;
in
a + b
3
자세한 설명
할당은 let과 in 키워드 사이에 둡니다. 이 예시에서는 a = 1과 b = a + 1을 할당합니다.
할당 순서는 중요하지 않습니다. 따라서 할당 순서를 거꾸로 한 다음 예시도 동등합니다:
let
a = 1;
b = a + 1;
in
a + b
3
b = a + 1의 a는 a = 1을 가리킨다는 점에 유의하십시오.
in 뒤에는 할당이 유효한 표현식이 옵니다. 이 예시에서 표현식은 a + b이며, 여기서 a는 a = 1을, b는 b = a + 1을 가리킵니다.
이름을 할당된 값으로 바꾸면 a + b는 3으로 평가됩니다.
이는 재귀 속성 집합과 비슷합니다. 두 경우 모두 할당 순서는 중요하지 않으며, 왼쪽의 이름을 할당(=) 오른쪽의 표현식에서 사용할 수 있습니다.
예시:
let ... in ...
let
b = a + 1;
c = a + b;
a = 1;
in { c = c; a = a; b = b; }
{ a = 1; b = 2; c = 3; }
rec { ... }
rec {
b = a + 1;
c = a + b;
a = 1;
}
{ a = 1; b = 2; c = 3; }
차이점은 재귀 속성 집합은 속성 집합으로 평가되지만, in 키워드 뒤에는 어떤 표현식이든 올 수 있다는 것입니다.
다음 예시에서는 let 표현식을 사용하여 리스트를 만듭니다.
let
b = a + 1;
c = a + b;
a = 1;
in [ a b c ]
[ 1 2 3 ]
새로 선언된 이름에는 let 표현식 자체 안에 있는 표현식만 접근할 수 있습니다. 이 바인딩은 지역 스코프를 가집니다.
반례:
{
a = let x = 1; in x;
b = x;
}
error: undefined variable 'x'
at «string»:3:7:
2| a = let x = 1; in x;
3| b = x;
| ^
4| }
속성 접근#
집합의 속성에는 점(.)과 속성 이름으로 접근합니다.
예시:
let
attrset = { x = 1; };
in
attrset.x
1
중첩된 속성에 접근하는 방식도 같습니다.
예시:
let
attrset = { a = { b = { c = 1; }; }; };
in
attrset.a.b.c
1
점(.) 표기법은 속성을 할당할 때도 사용할 수 있습니다.
예시:
{ a.b.c = 1; }
{ a = { b = { c = 1; }; }; }
with ...; ...#
with 표현식을 사용하면 속성 집합을 반복해서 참조하지 않고도 그 속성에 접근할 수 있습니다.
예시:
let
a = {
x = 1;
y = 2;
z = 3;
};
in
with a; [ x y z ]
[ 1 2 3 ]
다음 표현식은
with a; [ x y z ]
다음과 동등합니다.
[ a.x a.y a.z ]
with로 사용할 수 있게 된 속성은 세미콜론(;) 뒤에 오는 표현식의 스코프 안에서만 유효합니다.
반례:
let
a = {
x = 1;
y = 2;
z = 3;
};
in
{
b = with a; [ x y z ];
c = x;
}
error: undefined variable 'x'
at «string»:10:7:
9| b = with a; [ x y z ];
10| c = x;
| ^
11| }
inherit ...#
inherit은 기존 스코프에 있는 이름의 값을 중첩된 스코프의 같은 이름에 할당하는 축약 표현입니다. 같은 이름을 여러 번 반복하지 않도록 편의를 위해 존재합니다.
예시:
let
x = 1;
y = 2;
in
{
inherit x y;
}
{ x = 1; y = 2; }
다음 코드 조각은
inherit x y;
다음과 동등합니다.
x = x; y = y;
inherit (...) ...#
특정 속성 집합의 이름을 괄호로 감싸서 그 속성 집합에서 이름을 inherit할 수도 있습니다.
예시:
let
a = { x = 1; y = 2; };
in
{
inherit (a) x y;
}
{ x = 1; y = 2; }
다음 코드 조각은
inherit (a) x y;
다음과 동등합니다
x = a.x; y = a.y;
inherit는 let 표현식 안에서도 작동합니다.
예시:
let
a = { x = 1; y = 2; };
inherit (a) x y;
in [ x y ]
[ 1 2 ]
자세한 설명
이 예시는 인위적이지만, 더 복잡한 코드에서는 바깥 스코프의 이름을 다시 사용하는 중첩된 let 표현식을 자주 보게 됩니다.
여기서는 상속할 대상으로 사소하지 않은 값을 두기 위해 속성 집합 a = { x = 1; y = 2; }를 사용합니다. let 표현식은 ( )를 사용하여 a에서 x와 y를 상속하며, 이는 다음과 같이 작성하는 것과 동등합니다:
let
x = a.x;
y = a.y;
in
이제 새 내부 스코프에는 x와 y가 포함되며, 이들은 리스트 [ x y ]에서 사용됩니다.
문자열 보간 ${ ... }#
이전에는 “안티쿼테이션(antiquotation)”이라고 불렸습니다.
Nix 표현식의 값은 달러 기호와 중괄호(${ })를 사용하여 문자열에 삽입할 수 있습니다.
예시:
let
name = "Nix";
in
"hello ${name}"
"hello Nix"
문자열 또는 문자열로 표현할 수 있는 값만 허용됩니다.
반례:
let
x = 1;
in
"${x} + ${x} = ${x + x}"
error: cannot coerce an integer to a string
at «string»:4:2:
3| in
4| "${x} + ${x} = ${x + x}"
| ^
5|
보간된 표현식은 임의로 중첩할 수 있습니다.
(이렇게 하면 읽기 어려워질 수 있습니다. 실제로는 피하십시오.)
예시:
let
a = "no";
in
"${a + " ${a + " ${a}"}"}"
"no no no"
자세한 설명
값을 문자열로 표현할 수 있는 Nix 표현식이라면 어떤 것이든 ${ } 안에서 사용할 수 있습니다.
위 표현식의 + 기호는 문자열 연결 연산자로, 두 문자열을 받아 새 문자열을 만듭니다.
예시의 표현식은 문자열 보간을 임의로 중첩할 수는 있지만 읽기 어려워지기 쉽다는 점을 보여 주기 위해 일부러 헷갈리게 작성되었습니다.
이 표현식은 a의 값과, 공백으로 시작하고 그 뒤에 또 다른 보간된 문자열이 오는 문자열을 연결한 결과를 보간한 문자열을 나타냅니다. 두 번째 보간된 문자열 역시 a의 값과, 공백으로 시작하고 그 뒤에 a의 보간이 오는 또 다른 문자열을 연결한 결과입니다.
예시:
let
a = "one";
b = "two";
in
"${a + b}"
"onetwo"
내장 함수는 이후 섹션에서 다룹니다.
경고
할당된 이름 앞에 달러 기호($)는 있지만 중괄호({ })는 없는 문자열을 볼 수도 있습니다:
이러한 문자열은 보간된 문자열이 아니며, 보통 셸 스크립트의 변수를 나타냅니다.
이런 경우 주변 Nix 표현식의 이름이 사용된 것은 우연입니다.
예시:
let
out = "Nix";
in
"echo ${out} > $out"
"echo Nix > $out"
들여쓰기된 문자열#
“여러 줄 문자열(multi-line strings)”이라고도 합니다.
Nix 언어는 여러 줄에 걸쳐 있고 공통된 들여쓰기가 있는 문자열을 위한 편의 문법을 제공합니다.
들여쓰기된 문자열은 두 개의 작은따옴표('' '')로 표기합니다.
예시:
''
multi
line
string
''
"multi\nline\nstring\n"
각 줄 앞에 공통으로 붙은 같은 양의 공백은 결과에서 제거됩니다.
예시:
''
one
two
three
''
"one\n two\n three\n"
참고
들여쓰기된 문자열은 문자열 보간도 지원합니다. 자세한 내용은 Nix 언어의 문자열 리터럴에 관한 문서를 확인하십시오.
파일 시스템 경로#
Nix 언어는 파일 시스템 경로를 위한 편의 문법을 제공합니다.
절대 경로는 항상 슬래시(/)로 시작합니다.
예시:
/absolute/path
/absolute/path
경로에 슬래시(/)가 하나 이상 있지만 슬래시로 시작하지 않으면 상대 경로입니다. 상대 경로는 해당 표현식이 들어 있는 파일을 기준으로 한 경로로 평가됩니다.
다음 예시에서는 해당 Nix 파일이 /current/directory에 있다고(또는 nix repl을 /current/directory에서 실행한다고) 가정합니다.
예시:
./relative
/current/directory/relative
예시:
relative/path
/current/directory/relative/path
점 하나(.)는 주어진 경로 안에서 현재 디렉터리를 나타냅니다.
Nix 파일이 있는 디렉터리를 지정하는 다음 표현식을 자주 보게 될 것입니다.
예시:
./.
/current/directory
자세한 설명
상대 경로에는 슬래시(/)가 있어야 하지만 슬래시로 시작해서는 안 되고, 점(.)은 디렉터리를 바꾸지 않음을 뜻하므로, ./. 조합은 현재 디렉터리를 상대 경로로 지정합니다.
점 두 개(..)는 상위 디렉터리를 나타냅니다.
예시:
../.
/current
참고
경로는 보간된 표현식 안에서 사용할 수 있습니다. 이는 이후 섹션에서 자세히 다루는 순수하지 않은 연산입니다.
조회 경로#
“꺾쇠괄호 문법(angle bracket syntax)”이라고도 합니다.
예시:
<nixpkgs>
/nix/var/nix/profiles/per-user/root/channels/nixpkgs
조회 경로의 값은 builtins.nixPath의 값에 따라 달라지는 파일 시스템 경로입니다.
실제로 <nixpkgs>는 Nixpkgs의 특정 리비전이 있는 파일 시스템 경로를 가리킵니다.
예를 들어 <nixpkgs/lib>는 해당 파일 시스템 경로의 하위 디렉터리 lib를 가리킵니다:
<nixpkgs/lib>
/nix/var/nix/profiles/per-user/root/channels/nixpkgs/lib
이러한 예시를 많이 접하게 되겠지만, 조회 경로는 재현 가능하지 않은 비순수성이므로 프로덕션 코드에서는 조회 경로를 피하십시오.
함수#
함수는 Nix 언어 어디에나 등장하므로 특별히 주의를 기울일 필요가 있습니다.
함수는 항상 정확히 하나의 인자를 받습니다. 인자와 함수 본문은 콜론(:)으로 구분합니다.
Nix 언어 코드에서 콜론(:)이 보이면 어디서든 다음과 같습니다:
왼쪽에는 함수 인자가 있습니다
오른쪽에는 함수 본문이 있습니다.
함수 인자는 속성 집합과 let 표현식 외에 값에 이름을 붙이는 세 번째 방법입니다. 특히 값은 미리 알 수 없습니다. 이름은 함수를 호출할 때 채워지는 자리 표시자입니다.
Nix 언어의 함수 선언은 여러 가지 형태로 나타날 수 있습니다. 각 형태는 아래에서 차례로 설명하며, 개요는 다음과 같습니다:
단일 인자
x: x + 1
중첩을 통한 여러 인자
x: y: x + y
속성 집합 인수
{ a, b }: a + b
기본 속성 포함
{ a, b ? 0 }: a + b
추가 속성 허용
{ a, b, ...}: a + b
이름이 지정된 속성 집합 인수
args@{ a, b, ... }: a + b + args.c
또는
{ a, b, ... }@args: a + b + args.c
Nix 언어의 함수에는 이름이 없습니다. 함수는 익명이며, 이러한 함수를 람다라고 부릅니다.[1]
예시:
x: x + 1
<LAMBDA>
<LAMBDA>는 결괏값이 익명 함수임을 나타냅니다.
다른 값과 마찬가지로 함수에도 이름을 붙일 수 있습니다.
예시:
let
f = x: x + 1;
in f
<LAMBDA>
함수 호출#
“함수 적용”이라고도 합니다.
인자를 넘겨 함수를 호출한다는 것은 함수 뒤에 인자를 적는다는 뜻입니다.
예시:
let
f = x: x + 1;
in f 1
2
예시:
let
f = x: x.a;
in
f { a = 1; }
1
위 예시는 리터럴 속성 집합을 인자로 하여 f를 호출합니다. 인자를 이름으로 전달할 수도 있습니다.
예시:
let
f = x: x.a;
v = { a = 1; };
in
f v
1
함수와 인수는 공백으로 구분되므로, 원하는 결과를 얻으려면 괄호(( ))가 필요한 경우가 있습니다.
예시:
(x: x + 1) 1
2
자세한 설명
이 표현식은 익명 함수 x: x + 1을 인수 1에 적용합니다. 함수를 인수와 구별하려면 함수를 괄호로 감싸야 합니다.
예시:
리스트 요소 역시 공백으로 구분되므로, 다음은 서로 다릅니다:
let
f = x: x + 1;
a = 1;
in [ (f a) ]
[ 2 ]
let
f = x: x + 1;
a = 1;
in [ f a ]
[ <LAMBDA> 1 ]
첫 번째 예시는 f를 a에 적용하고 그 결과를 리스트에 넣으라는 뜻입니다. 결과 리스트에는 원소가 하나 있습니다.
두 번째 예시는 f와 a를 리스트에 넣으라는 뜻입니다. 결과 리스트에는 원소가 두 개 있습니다.
여러 인수#
“커링된 함수”라고도 합니다.
Nix 함수는 정확히 하나의 인수를 받습니다. 여러 인수는 함수를 중첩하여 처리할 수 있습니다.
이렇게 중첩된 함수는 여러 인수를 받는 함수처럼 사용할 수 있으면서도 더 많은 유연성을 제공합니다.
예시:
x: y: x + y
<LAMBDA>
위 함수는 다음과 동일합니다.
x: (y: x + y)
<LAMBDA>
이 함수는 인수 하나를 받아, x가 그 인수의 값으로 설정된 또 다른 함수 y: x + y를 반환합니다.
예시:
let
f = x: y: x + y;
in
f 1
<LAMBDA>
f 1의 결과로 나온 함수를 다른 인수에 적용하면 내부 본문 x + y(x는 1로, y는 그 다른 인수로 설정됨)가 나오며, 이제 이를 완전히 평가할 수 있습니다.
let
f = x: y: x + y;
in
f 1 2
3
속성 집합 인수#
“키워드 인수” 또는 “구조 분해”라고도 합니다.
Nix 함수는 특정 구조를 가진 속성 집합을 인수로 요구하도록 선언할 수 있습니다.
이는 기대하는 속성 이름을 쉼표(,)로 구분하여 나열하고 중괄호({ })로 감싸서 나타냅니다.
예시:
{a, b}: a + b
<LAMBDA>
인수는 해당 집합에 반드시 있어야 하는 속성을 정확히 정의합니다. 속성을 빠뜨리거나 추가 속성을 전달하면 오류가 발생합니다.
예시:
let
f = {a, b}: a + b;
in
f { a = 1; b = 2; }
3
반례:
let
f = {a, b}: a + b;
in
f { a = 1; b = 2; c = 3; }
error: 'f' at (string):2:7 called with unexpected argument 'c'
at «string»:4:1:
3| in
4| f { a = 1; b = 2; c = 3; }
| ^
5|
기본값#
“기본 인수(default arguments)”라고도 합니다.
구조 분해된 인수는 속성에 대한 기본값을 가질 수 있습니다.
속성 이름과 기본값을 물음표(?)로 구분하여 표기합니다.
인수의 속성에 기본값이 있으면 그 속성은 필수가 아닙니다.
예시:
let
f = {a, b ? 0}: a + b;
in
f { a = 1; }
1
예시:
let
f = {a ? 0, b ? 0}: a + b;
in
f { } # empty attribute set
0
추가 속성#
말줄임표(...)를 사용하면 추가 속성을 허용할 수 있습니다:
{a, b, ...}: a + b
앞의 반례와 달리, 추가 속성이 포함된 인수를 전달해도 오류가 발생하지 않습니다.
예시:
let
f = {a, b, ...}: a + b;
in
f { a = 1; b = 2; c = 3; }
3
이름이 지정된 속성 집합 인수#
“@ 패턴”, “@ 구문” 또는 “‘at’ 구문”이라고도 합니다.
속성 집합 인수 전체에 접근할 수 있도록 이름을 붙일 수 있습니다.
속성 집합 인수의 앞이나 뒤에 이름을 붙이고 골뱅이 기호(@)로 구분하여 표기합니다.
예시:
{a, b, ...}@args: a + b + args.c
<LAMBDA>
또는
args@{a, b, ...}: a + b + args.c
<LAMBDA>
예시:
let
f = {a, b, ...}@args: a + b + args.c;
in
f { a = 1; b = 2; c = 3; }
6
함수 라이브러리#
내장 연산자(+, ==, && 등) 외에도, 함께 Nix 언어의 표준으로 볼 수 있는 널리 사용되는 라이브러리가 두 가지 있습니다. Nix 언어 코드를 이해하고 탐색하려면 두 라이브러리를 모두 알아야 합니다.
어떤 기능이 제공되는지 익숙해지도록 대략 훑어보십시오.
builtins#
“기본 연산(primitive operations)” 또는 “primops”라고도 합니다.
Nix에는 언어에 내장된 함수가 많이 있습니다. 이 함수들은 Nix 언어 인터프리터의 일부로서 C++로 구현되어 있습니다.
참고
Nix 매뉴얼에는 모든 내장 함수와 그 사용법이 나와 있습니다.
이 함수들은 builtins 상수를 통해 사용할 수 있습니다.
예시:
builtins.toString
<PRIMOP>
import#
대부분의 내장 함수는 builtins를 통해서만 접근할 수 있습니다. 주목할 만한 예외는 import로, 최상위 수준에서도 사용할 수 있습니다.
import는 Nix 파일의 경로를 받아 파일을 읽고, 그 안에 담긴 Nix 표현식을 평가한 다음 결과 값을 반환합니다. 경로가 디렉터리를 가리키면 그 디렉터리의 default.nix 파일이 대신 사용됩니다.
예시:
$ echo 1 + 2 > file.nix
import ./file.nix
3
자세한 설명
앞의 셸 명령은 현재 디렉터리의 file.nix 파일에 1 + 2라는 내용을 씁니다.
위의 Nix 표현식은 이 파일을 ./file.nix로 참조합니다. import는 파일을 읽고, 그 안에 담긴 Nix 표현식으로 평가됩니다.
파일 시스템 경로가 존재하지 않으면 오류가 발생합니다.
file.nix를 읽고 나면 이 Nix 표현식은 파일 내용과 동일합니다:
1 + 2
3
Nix 파일에는 어떤 Nix 표현식이든 담을 수 있으므로, import로 가져온 함수를 곧바로 인수에 적용할 수 있습니다.
import 호출 뒤에 추가 토큰이 있다면 반환된 값은 함수입니다. 그 뒤에 오는 것은 모두 해당 함수의 인수입니다.
예시:
$ echo "x: x + 1" > file.nix
import ./file.nix 1
2
자세한 설명
앞의 셸 명령은 현재 디렉터리의 file.nix 파일에 x: x + 1이라는 내용을 씁니다.
위의 Nix 표현식은 이 파일을 ./file.nix로 참조합니다. import ./file.nix는 파일을 읽고, 그 안에 담긴 Nix 표현식으로 평가됩니다.
파일 시스템 경로가 존재하지 않으면 오류가 발생합니다.
파일을 읽고 나면 Nix 표현식 import ./file.nix는 파일 내용과 동일합니다:
(x: x + 1) 1
2
이는 함수 x: x + 1을 인수 1에 적용하므로 2로 평가됩니다.
참고
함수 선언과 함수 적용을 구분하려면 괄호가 필요합니다.
pkgs.lib#
nixpkgs 저장소에는 lib이라는 속성 집합이 있으며, 이 속성 집합은 유용한 함수를 많이 제공합니다. 이 함수들은 언어 자체의 일부인 builtins와 달리 Nix 언어로 구현되어 있습니다.
참고
Nixpkgs 매뉴얼에는 모든 Nixpkgs 라이브러리 함수가 나열되어 있습니다.
관례상 Nixpkgs 속성 집합에 pkgs라는 이름을 붙이므로, 이 함수들에는 보통 pkgs.lib을 통해 접근합니다.
예시:
let
pkgs = import <nixpkgs> {};
in
pkgs.lib.strings.toUpper "lookup paths considered harmful"
LOOKUP PATHS CONSIDERED HARMFUL
자세한 설명
더 복잡한 예시이지만, 이제는 모든 구성 요소가 익숙할 것입니다.
pkgs라는 이름은 어떤 파일에서 import로 가져온 표현식으로 선언됩니다. 그 파일의 경로는 조회 경로 <nixpkgs>의 값으로 결정되며, 이 값은 다시 이 표현식이 평가되는 시점의 $NIX_PATH 환경 변수로 결정됩니다. 이 표현식은 마침 함수이므로 평가하려면 인수가 필요하며, 이 경우에는 빈 속성 집합 {}를 전달하는 것으로 충분합니다.
이제 pkgs가 let ... in ...의 스코프 안에 있으므로 그 속성에 접근할 수 있습니다. Nixpkgs 매뉴얼을 보면 lib.strings.toUpper 아래에 함수가 있음을 알 수 있습니다.
간결하게 하기 위해 이 예제에서는 조회 경로를 사용하여 Nixpkgs의 어떤 버전을 가져옵니다. toUpper 함수는 충분히 단순하므로 Nixpkgs 버전이 달라도 다른 결과를 내지 않으리라 예상할 수 있습니다. 하지만 더 복잡한 소프트웨어는 이러한 문제를 겪을 가능성이 높습니다. 따라서 완전히 재현 가능한 예제는 다음과 같습니다.
let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/archive/06278c77b5d162e62df170fec307e83f1812d94b.tar.gz";
pkgs = import nixpkgs {};
in
pkgs.lib.strings.toUpper "always pin your sources"
ALWAYS PIN YOUR SOURCES
자세한 내용은 재현성을 향하여: Nixpkgs 고정하기 문서를 참고하십시오.
또한 pkgs가 함수의 인자로 전달되는 경우도 자주 볼 수 있습니다. 관례상 이는 lib 속성을 가진 Nixpkgs 속성 집합을 가리킨다고 가정할 수 있습니다.
{ pkgs, ... }:
pkgs.lib.strings.removePrefix "no " "no true scotsman"
<LAMBDA>
이 함수가 결과를 내도록 하려면 파일(예: file.nix)에 작성한 뒤 nix-instantiate를 통해 인자를 전달하면 됩니다.
$ nix-instantiate --eval file.nix --arg pkgs 'import <nixpkgs> {}'
"true scotsman"
NixOS 설정에서, 그리고 Nixpkgs 내부에서도 lib이 직접 전달되는 경우를 자주 볼 수 있습니다. 이 경우 이 lib은 pkgs만 사용할 수 있는 곳의 pkgs.lib과 동일하다고 가정할 수 있습니다.
예시:
{ lib, ... }:
let
to-be = true;
in
lib.trivial.or to-be (! to-be)
<LAMBDA>
이 함수가 결과를 내도록 하려면 파일(예: file.nix)에 작성한 뒤 nix-instantiate를 통해 인자를 전달하면 됩니다.
$ nix-instantiate --eval file.nix --arg lib '(import <nixpkgs> {}).lib'
true
때로는 pkgs와 lib이 모두 인자로 전달되기도 합니다. 이 경우 pkgs.lib과 lib은 동일하다고 가정할 수 있습니다. 이는 pkgs.lib을 반복해서 사용하지 않도록 하여 가독성을 높이기 위한 것입니다.
예시:
{ pkgs, lib, ... }:
# ... multiple uses of `pkgs`
# ... multiple uses of `lib`
역사적인 이유로 pkgs.lib의 일부 함수는 같은 이름의 builtins와 동일합니다.
비순수성#
지금까지 이 튜토리얼에서는 순수 표현식, 즉 데이터를 선언하고 함수로 변환하는 것만 다루었습니다.
실제로 derivation을 기술하려면 외부 세계를 관찰해야 합니다. derivation은 파일 시스템을 대상으로 함수형 프로그래밍을 가능하게 하는 Nix 언어의 핵심 기능입니다. derivation은 튜토리얼 뒷부분에서 다룹니다.
여기서 관련된 Nix 언어의 비순수성은 단 하나, 파일 시스템에서 파일을 빌드 입력으로 읽어 들이는 것입니다.
derivation은 새 파일을 도출하는 방법을 기술하기 위해 빌드 입력을 참조합니다. derivation은 실행될 때 명시적으로 선언된 빌드 입력에만 접근할 수 있습니다.
Nix 언어에서 빌드 입력을 지정하는 유일한 방법은 다음을 명시적으로 사용하는 것입니다.
파일 시스템 경로
전용 함수
Nix와 Nix 언어는 파일을 내용 해시로 참조합니다. 파일 내용을 미리 알 수 없다면 표현식 평가 중에 파일을 읽는 것은 피할 수 없습니다.
참고
Nix는 조회 경로나 상수 builtins.currentSystem과 같은 다른 유형의 비순수 표현식도 지원합니다. 이들은 Nix 언어가 원칙적으로 작동하는 방식과는 관계가 없고, 바로 재현성을 해친다는 이유로 권장되지 않으므로 여기서는 더 자세히 다루지 않습니다.
경로#
파일 시스템 경로가 문자열 보간에 사용될 때마다, 부수 효과로 해당 파일의 내용이 파일 시스템의 특별한 위치인 Nix 스토어에 복사됩니다.
그러면 평가된 문자열에는 해당 파일에 할당된 Nix 스토어 경로가 포함됩니다.
예시:
$ echo 123 > data
"${./data}"
"/nix/store/h1qj5h5n05b5dl5q4nldrqq8mdg7dhqk-data"
자세한 설명
앞의 셸 명령은 현재 디렉터리의 data 파일에 문자 123을 기록합니다.
위의 Nix 표현식은 이 파일을 ./data로 참조하고, 파일 시스템 경로를 보간된 문자열 ${ ... }로 변환합니다.
이러한 보간된 표현식은 문자열로 표현할 수 있는 값으로 평가되어야 합니다. 파일 시스템 경로가 바로 그러한 값이며, 그 문자열 표현은 해당하는 Nix 스토어 경로입니다.
/nix/store/<hash>-<name>
Nix 스토어 경로는 파일 내용의 해시(<hash>)를 구하고 이를 파일 이름(<name>)과 결합하여 얻습니다. 평가의 부수 효과로 파일이 Nix 스토어 디렉터리 /nix/store에 복사됩니다. 파일 시스템 경로가 존재하지 않으면 오류가 발생합니다.
디렉터리도 마찬가지입니다. 디렉터리 전체(중첩된 파일과 디렉터리 포함)가 Nix 스토어에 복사되고, 평가된 문자열은 해당 디렉터리의 Nix 스토어 경로가 됩니다.
페처#
빌드 입력으로 사용할 파일이 반드시 파일 시스템에서 와야 하는 것은 아닙니다.
Nix 언어는 평가 중에 네트워크를 통해 파일을 가져오는 비순수 내장 함수를 제공합니다.
이 함수들은 Nix 스토어 안의 파일 시스템 경로로 평가됩니다.
예시:
builtins.fetchurl "https://github.com/NixOS/nix/archive/7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz"
"/nix/store/7dhgs330clj36384akg86140fqkgh8zf-7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz"
이 중 일부는 아카이브 자동 압축 해제와 같은 편의 기능을 추가로 제공합니다.
예시:
builtins.fetchTarball "https://github.com/NixOS/nix/archive/7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz"
"/nix/store/d59llm96vgis5fy231x6m7nrijs0ww36-source"
참고
페처에 관한 Nixpkgs 매뉴얼에는 네트워크를 통해 파일을 가져오는 다양한 추가 라이브러리 함수가 나와 있습니다.
네트워크 요청이 실패하면 오류가 발생합니다.
Derivation#
Derivation은 Nix와 Nix 언어 모두의 핵심입니다.
Nix 언어는 derivation을 기술하는 데 사용됩니다.
Nix는 derivation을 실행하여 빌드 결과를 생성합니다.
빌드 결과는 다시 다른 derivation의 입력으로 사용될 수 있습니다.
derivation을 선언하는 Nix 언어의 기본 요소는 비순수 내장 함수 derivation입니다.
이 함수는 보통 Nixpkgs 빌드 메커니즘인 stdenv.mkDerivation으로 감싸서 사용하며, 이 메커니즘은 간단하지 않은 빌드 절차에 따르는 복잡성의 상당 부분을 감춰 줍니다.
참고
실제로 derivation을 직접 접할 일은 아마 없을 것입니다.
mkDerivation이 보이면 언제나, 그것은 Nix가 결국 빌드할 무언가를 나타냅니다.
derivation(및 mkDerivation)의 평가 결과는 특정 구조와 특별한 성질을 가진 속성 집합입니다. 이 속성 집합은 문자열 보간에 사용할 수 있으며, 이 경우 빌드 결과의 Nix 스토어 경로로 평가됩니다.
예시:
let
pkgs = import <nixpkgs> {};
in "${pkgs.nix}"
"/nix/store/sv2srrjddrp2isghmrla8s6lazbzmikd-nix-2.11.0"
참고
출력 결과는 다를 수 있습니다. 다른 해시가 생성되거나 심지어 다른 패키지 버전이 나올 수도 있습니다.
derivation의 출력 경로는 입력에 의해 완전히 결정되며, 이 경우 입력은 Nixpkgs의 어떤 버전에서 옵니다.
이것이 설명만을 위한 예시를 제외하고는 예측 가능한 결과를 보장하기 위해 조회 경로를 피해야 하는 이유입니다.
자세한 설명
이 예시는 조회 경로 <nixpkgs>에서 Nix 표현식을 가져오고, 그 결과로 얻은 함수를 빈 속성 집합 {}에 적용합니다. 그 출력에는 pkgs라는 이름이 할당됩니다.
pkgs.nix는 derivation이므로, 문자열 보간으로 속성 pkgs.nix를 문자열로 변환할 수 있습니다. 즉, pkgs.nix는 결국 derivation 호출로 귀결됩니다.
결과 문자열은 해당 derivation의 빌드 결과가 최종적으로 위치하게 될 파일 시스템 경로입니다.
derivation의 내부 동작에는 더 깊은 내용이 있지만, 현재로서는 이러한 표현식이 Nix 스토어 경로로 평가된다는 것만 알아도 충분합니다.
derivation에 대한 문자열 보간은 새로운 derivation을 선언할 때 해당 derivation의 빌드 결과를 파일 시스템 경로로 참조하는 데 사용됩니다.
이를 통해 Nix 언어로 임의로 복잡한 derivation 조합을 구성할 수 있습니다.
실전 예시#
지금까지의 예시는 Nix 언어 구문을 인위적으로 보여 주는 예시였습니다.
이제 간단한 패키지와 설정에 대한 Nix 언어 코드를 읽을 수 있어야 하며, 다음의 실용적인 예시에 대해서도 비슷한 설명을 할 수 있어야 합니다.
참고
다음 연습의 목표는 코드가 무엇을 의미하는지나 어떻게 동작하는지를 이해하는 것이 아니라, 코드가 함수, 속성 집합 및 기타 Nix 언어 데이터 타입의 관점에서 어떻게 구성되어 있는지를 이해하는 것입니다.
셸 환경#
{ pkgs ? import <nixpkgs> {} }:
let
message = "hello world";
in
pkgs.mkShellNoCC {
packages = with pkgs; [ cowsay ];
shellHook = ''
cowsay ${message}
'';
}
이 예시는 셸 환경(초기화 시 shellHook을 실행함)을 선언합니다.
설명:
이 표현식은 속성 집합을 인수로 받는 함수입니다.
인수에 속성
pkgs가 있으면 그것이 함수 본문에서 사용됩니다. 그렇지 않으면 기본적으로 조회 경로<nixpkgs>에서 찾은 파일의 Nix 표현식(이 경우 함수임)을 가져와, 빈 속성 집합으로 해당 함수를 호출하고, 그 결과 값을 사용합니다.이름
message는 문자열 값"hello world"에 바인딩됩니다.pkgs집합의 속성mkShellNoCC는 속성 집합을 인수로 전달받는 함수입니다. 그 반환 값은 바깥 함수의 결과이기도 합니다.mkShellNoCC에 전달되는 속성 집합에는 속성packages(원소가 하나인 리스트로 설정됨:pkgs의cowsay속성)와shellHook(들여쓰기된 문자열로 설정됨)이 있습니다.들여쓰기된 문자열에는 보간된 표현식이 포함되어 있으며, 이 표현식은
message의 값을 전개하여"hello world"를 만들어 냅니다.
NixOS 설정#
{ config, pkgs, ... }: {
imports = [ ./hardware-configuration.nix ];
environment.systemPackages = with pkgs; [ git ];
# ...
}
이 예제는 NixOS 설정(의 일부)입니다.
설명:
이 표현식은 속성 집합을 인수로 받는 함수입니다. 이 함수는 속성 집합을 반환합니다.
인수는 최소한
config와pkgs속성을 가져야 하며, 그 외의 속성을 더 가질 수도 있습니다.반환되는 속성 집합에는
imports와environment속성이 들어 있습니다.imports는 요소가 하나인 리스트이며, 그 요소는 이 Nix 파일과 같은 위치에 있는hardware-configuration.nix라는 파일의 경로입니다.참고
imports는 순수하지 않은 내장 함수import가 아니라 일반적인 속성 이름입니다!environment는 그 자체로systemPackages속성 하나를 가진 속성 집합이며, 이 속성은 요소가 하나인 리스트로 평가됩니다. 그 요소는pkgs집합의git속성입니다.config인수는 (여기에 보이는 부분에서는) 사용되지 않습니다.
패키지#
{ lib, stdenv, fetchurl }:
stdenv.mkDerivation rec {
pname = "hello";
version = "2.12";
src = fetchurl {
url = "mirror://gnu/${pname}/${pname}-${version}.tar.gz";
sha256 = "1ayhp9v4m4rdhjmnl2bq3cibrbqqkgjbl3s7yk2nhlh8vj3ay16g";
};
meta = with lib; {
license = licenses.gpl3Plus;
};
}
이 예제는 Nixpkgs의 (단순화한) 패키지 선언입니다.
설명:
이 표현식은 정확히
lib,stdenv,fetchurl속성만 가져야 하는 속성 집합을 인수로 받는 함수입니다.이 함수는
stdenv의 속성인mkDerivation함수를 재귀 집합에 적용하여 평가한 결과를 반환합니다.mkDerivation에 전달되는 재귀 집합은fetchurl함수의 인수에서 자신의pname과version속성을 사용합니다.fetchurl자체는 바깥쪽 함수의 인수에서 옵니다.meta속성은 그 자체로 속성 집합이며, 그 안의license속성은 중첩된 속성lib.licenses.gpl3Plus에 할당된 값을 가집니다.
참고 자료#
다음 단계#
실제로 활용하기#
shell.nix를 사용한 선언적 셸 환경 – Nix 파일로 재현 가능한 셸 환경 만들기
Nix로 기존 소프트웨어 패키징하기 – Nix를 통해 더 많은 소프트웨어를 사용할 수 있게 만들기
Nix 학습을 한동안 쉬려는 경우, 다음 명령으로 Nix 스토어에서 사용하지 않는 빌드 결과를 제거할 수 있습니다:
$ nix-collect-garbage
더 알아보기#
예제를 직접 따라 해 보셨다면, Nix 언어를 읽으면 코드의 구조는 드러나지만 코드가 실제로 무엇을 의미하는지까지 반드시 알 수 있는 것은 아니라는 점을 알아차리셨을 것입니다.
주어진 코드만으로는 다음을 판단할 수 없는 경우가 많습니다
이름이 붙은 값이나 함수 인자의 데이터 타입.
호출된 함수가 인자로 받는 데이터 타입.
주어진 속성 집합에 어떤 속성이 있는지.
예시:
{ x, y, z }: (x y) z.a
다음을 어떻게 알 수 있습니까…
x가 인자를 받으면 함수를 반환하는 함수라는 것을?x가 함수라고 할 때,y가x에 적절한 인자라는 것을?(x y)가 함수라고 할 때,z.a가(x y)에 적절한 인자라는 것을?z가 애초에 속성 집합이라는 것을?z가 속성 집합이라고 할 때,a라는 속성을 가지고 있다는 것을?y와z.a가 어떤 데이터 타입인지를?최종 결과의 데이터 타입을?
그리고 이 함수를 호출하는 쪽은 이 함수가 x, y, z 속성을 가진 속성 집합을 요구한다는 것을 어떻게 알 수 있습니까?
이러한 질문에 답하려면 주어진 표현식이 어떤 맥락에서 사용되도록 되어 있는지 알아야 합니다.
Nix 생태계와 코드 스타일은 관례에 따라 움직입니다. Nix 언어 코드에서 접하게 될 이름 대부분은 Nixpkgs에서 온 것입니다:
Nix Pills - derivation과 Nixpkgs가 기본 원리부터 어떻게 구성되는지에 대한 자세한 설명
Nixpkgs는 널리 사용되는 범용 빌드 메커니즘을 제공합니다:
Nixpkgs의 패키지는 여러 메커니즘을 통해 수정할 수 있습니다:
오버라이드 – 특히 개별 패키지를 수정하기 위한
override와overrideAttrs오버레이 – 개별적으로 수정된 패키지를 포함하는 Nixpkgs의 사용자 정의 변형을 만들기 위한 것
언어 생태계와 프레임워크마다 Nixpkgs에 수용되기 위한 요구 사항이 서로 다릅니다:
언어 및 프레임워크에는 Nix로 특정 언어나 프레임워크용 패키지를 빌드하기 위해 Nixpkgs가 제공하는 도구가 나열되어 있습니다.
NixOS Linux 배포판은 고유한 관례를 따르도록 하는 모듈식 설정 시스템을 사용합니다.