Functions and Import

Welcome to the fifth Nix pill. In the previous fourth pill, we started learning the Nix programming language. We talked about the basic types and meanings of the language, and about basic expressions, such as if, with And let. To reinforce the material, start a REPL session and experiment with Nix expressions.

Functions part are used to build reusable components in large repositories, say in nixpkgs. The Nix Language Guide has [великолепное объяснение функций] (https://nixos.org/manual/nix/stable/expressions/language-constructs.html#functions), so I'll be referring to it often.

As a reminder, how to start a Nix environment: source ~/.nix-profile/etc/profile.d/nix.sh

Unnamed with a single parameter

In Nix, functions are always anonymous (that is, they are lambdas) and they always have one parameter. The syntax is extremely simple: write the parameter name, then “:“, then the function body.

nix-repl> x: x*2
«lambda»

Here we have defined a function that takes a parameter x and returns x*2. The problem is that we can't call her because she doesn't have a name… just kidding!

We can give a function a name by associating it with a variable.

nix-repl> double = x: x*2
nix-repl> double
«lambda»
nix-repl> double 3
6

As I wrote earlier, assignment exists only in nix repl, it doesn't exist in regular Nix language. So we have defined the function x: x*2which takes one parameter x and returns x*2. This function is then assigned to a variable double. After this, the function can be called: double 3.

Important Note: In most programming languages, parameters must be enclosed in parentheses: double(3). In Nix, parentheses are not needed: double 3.

Summary: To call a function, write its name, then a space, then an argument. It's that simple.

When there is more than one parameter

How to write a function that takes more than one parameter? For those new to functional programming, it will take a little time to figure it out. Let's explore the topic step by step.

nix-repl> mul = a: (b: a*b)
nix-repl> mul
«lambda»
nix-repl> mul 3
«lambda»
nix-repl> (mul 3) 4
12

First we defined a function that takes a parameter a and returns another function. This other function takes a parameter b and returns a*b. Calling mul 3 we get the function as a result b: 3*b. Calling it with a parameter 4we get the desired result.

In this code, you can skip the parentheses altogether, since Nix has operator precedence:

nix-repl> mul = a: b: a*b
nix-repl> mul
«lambda»
nix-repl> mul 3
«lambda»
nix-repl> mul 3 4
12
nix-repl> mul (6+7) (8+9)
221

Everything looks like the function mul two parameters. Because arguments are separated by space, parentheses are needed to pass more complex expressions. In other languages ​​you would write mul(6+7, 8+9).

Since functions only have one parameter, it's easy to use partial application:

nix-repl> foo = mul 3
nix-repl> foo 4
12
nix-repl> foo 5
15

We saved the function that was returned mul 3 into a variable fooand then called.

Argument set

One of the most powerful features of Nix is ​​pattern matching of a parameter, which is of type attribute set. Let's write an alternative version mul = a: b: a*b first using a set of arguments and then pattern matching.

nix-repl> mul = s: s.a*s.b
nix-repl> mul { a = 3; b = 4; }
12
nix-repl> mul = { a, b }: a*b
nix-repl> mul { a = 3; b = 4; }
12

In the first case, we defined a function that takes one set parameter. Then we took the attributes a And b from this set. Notice how elegant the call entry looks without parentheses. In other languages ​​we would have to write mul({ a=3; b=4; }).

In the second case, we defined a set of arguments. This is similar to defining a set of attributes, but without the values. We require that the passed set contains the keys a And b. Then we can use these a And b directly in the function body.

nix-repl> mul = { a, b }: a*b
nix-repl> mul { a = 3; b = 4; c = 6; }
error: anonymous function at (string):1:2 called with unexpected argument `c', at (string):1:1
nix-repl> mul { a = 3; }
error: anonymous function at (string):1:2 called without required argument `b', at (string):1:1

The function accepts a set with exactly the attributes that were specified when it was defined.

Default and Variable Attributes

You can specify in the argument set default attribute values:

nix-repl> mul = { a, b ? 2 }: a*b
nix-repl> mul { a = 3; }
6
nix-repl> mul { a = 3; b = 4; }
12

A function may take more attributes than it needs. Such attributes are called variable:

nix-repl> mul = { a, b, ... }: a*b
nix-repl> mul { a = 3; b = 4; c = 2; }

Here you can't access the attribute c. But you can access any attributes by naming the entire set with @-pattern:

nix-repl> mul = s@{ a, b, ... }: a*b*s.c
nix-repl> mul { a = 3; b = 4; c = 2; }
24

Having written name@ before the sample, you give a name name the entire set of attributes.

Advantages of using argument sets:

  • Because the arguments are named, you don't have to remember their order.

  • You can pass a set as arguments, which creates a whole new level of flexibility and convenience.

Flaws:

  • Partial application does not work with sets of arguments. You must define a set of attributes as a whole; you cannot define only part of it.

Attribute sets are similar to **kwargs from Python.

Import

Function built into the language import allows you to include other files in the program text .nix. This approach is quite common in programming: we define each component in a separate file .nixand then we connect the components by importing these files into one module.

Let's start with the simplest example.

a.nix:

3

b.nix:

4

mul.nix:

a: b: a*b
nix-repl> a = import ./a.nix
nix-repl> b = import ./b.nix
nix-repl> mul = import ./mul.nix
nix-repl> mul a b
12

Yes, it really is that simple. You import the file, it is compiled into an expression. An important point: the importing file does not have access to variables from the imported file.

test.nix:

x
nix-repl> let x = 5; in import ./test.nix
error: undefined variable `x' at /home/lethal/test.nix:1:1

To pass information to the imported module, you need to use functions. A more complicated example:

test.nix:

{ a, b ? 3, trueMsg ? "yes", falseMsg ? "no" }:
if a > b
  then builtins.trace trueMsg true
  else builtins.trace falseMsg false
nix-repl> import ./test.nix { a = 5; trueMsg = "ok"; }
trace: ok
true

Explanation:

  • IN text.nix we return a function. It accepts a set where the attributes b, trueMsg And falseMsg there are default values.

  • builtins.tracebuilt-in function, which takes two arguments. The first is the print message, the second is the return value. It is usually used for debugging.

  • At the end we import text.nix and call the function with the set { a = 5; trueMsg = "ok"; }.

When will the message be printed? Then, when the calculations reach the corresponding branch of the code.

In the next pill

…we will finally write our first creation.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *