Skip to content
Open

lib: etc #113661

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ let
trivial = callLibs ./trivial.nix;
fixedPoints = callLibs ./fixed-points.nix;

# control flow
switchs = callLibs ./switchs.nix;

# datatypes
attrsets = callLibs ./attrsets.nix;
lists = callLibs ./lists.nix;
strings = callLibs ./strings.nix;
stringsWithDeps = callLibs ./strings-with-deps.nix;
preds = callLibs ./preds.nix;

# packaging
customisation = callLibs ./customisation.nix;
Expand Down Expand Up @@ -71,6 +75,7 @@ let
toHexString toBaseDigits;
inherit (self.fixedPoints) fix fix' converge extends composeExtensions
composeManyExtensions makeExtensible makeExtensibleWithCustomName;
inherit (self.switchs) switch-if switch;
inherit (self.attrsets) attrByPath hasAttrByPath setAttrByPath
getAttrFromPath attrVals attrValues getAttrs catAttrs filterAttrs
filterAttrsRecursive foldAttrs collect nameValuePair mapAttrs
Expand All @@ -81,7 +86,7 @@ let
getLib getDev getMan chooseDevOutputs zipWithNames zip
recurseIntoAttrs dontRecurseIntoAttrs cartesianProductOfSets;
inherit (self.lists) singleton forEach foldr fold foldl foldl' imap0 imap1
concatMap flatten remove findSingle findFirst any all count
concatMap flatten remove findSingle findFirst splitList any all count
optional optionals toList range partition zipListsWith zipLists
reverseList listDfs toposort sort naturalSort compareLists take
drop sublist last init crossLists unique intersectLists
Expand Down
40 changes: 40 additions & 0 deletions lib/lists.nix
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,46 @@ rec {
let found = filter pred list;
in if found == [] then default else head found;

/* Takes a predicate and a list as input and
returns a list of lists, separated by elements that match the predicate.

This is analoguous to `builtins.split separator string`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if we should use a stronger name like splitIf or splitListIf to disambiguate from builtins.split?

with a predicate instead of a separator
and a list instead of a string.

Type: splitList :: (a -> bool) -> [a] -> [ (a | [a]) ]

The returned list always has the form `[ [...] sep [...] ... sep [...] ]`
i.e. begins with a list, alternates with separators and ends with a list,
so that the following is true for `res = splitList pred l`:
- `res` has length `2 * k + 1`,
where `k` is the number of elements of `l` that
match `pred`. (ie `splitList pred l`)
- `elemAt res (2 * n)` is always a list,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add “where n is a number smaller than builtins.div (builtins.length l) 2. Same for the next property.

which contains no element matching `pred`.
- `elemAt res (2 * n + 1)` is always a single element that matches `pred`.

Ie, for all `pred` and `l`, let `k = length (filter pred l)`, we have:
- `length (splitList pred l) = 2 * k + 1`,
- `isList (elemAt (splitList pred l) (2 * n))`, for all `n <= k`,
- `pred (elemAt (splitList pred l) (2 * n + 1))`, for all `n < k`,
- `flatten (map (pick (splitList pred l)) (range 0 (2 * k))) == l`, where
`pick = r: n: (if mod n 2 == 1 then singleton else id) (elemAt r n)`

Examples:
- splitList (x: x == "x") [ "y" "x" "z" "t" ]
=> [ [ "y" ] "x" [ "z" "t" ] ]
- splitList (x: false) l == [ l ]
- splitList (x: x < 2) [ 2 1 3 4 0 1 3 1 ]
=> [ [ 2 ] 1 [ 3 4 ] 0 [ ] 1 [ 3 ] 1 [ ] ]

*/
splitList = pred: l:
let loop = (vv: v: l: if l == [] then vv ++ [v]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parentheses should be unnecessary here.

else let hd = head l; tl = tail l; in
if pred hd then loop (vv ++ [ v hd ]) [] tl else loop vv (v ++ [hd]) tl);
in loop [] [] l;

/* Return true if function `pred` returns true for at least one
element of `list`.

Expand Down
17 changes: 17 additions & 0 deletions lib/preds.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{ lib }:
with lib; {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document these individually, so nixdoc can pick them up properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will rename preds.true to preds.any and preds.false to preds.none to make the problematic example of @edolstra ["8.13" preds.true] read ["8.13" preds.any] instead, which looks more readable IMO.

/* Constant predicates: true and false */
true = p: true;
false = p: false;

/* Predicate intersection, union, complement,
difference and implication */
inter = p: q: x: p x && q x;
union = p: q: x: p x || q x;
compl = p: x: ! p x;
diff = p: q: preds.inter p (preds.compl q);
impl = p: q: x: p x -> q x;

/* predicate "being equal to x" */
equal = x: y: y == x;
}
75 changes: 75 additions & 0 deletions lib/switchs.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{ lib }:
with lib; {
/* Emulate a "switch - case" construct,
instead of relying on `if then else if ...` */

/* Usage:
```nix
switch-if [
if-clause-1
..
if-clause-k
] default-out
```
where a if-clause has the form `{ cond = b; out = r; }`
the first branch such as `b` is true

Example:
```nix
let alphanum = n: switch-if [
{ cond = n == 0; out = "zero"; }
{ cond = n == 1; out = "one"; }
] "I do not count beyond one"; in
[ (alphanum 0) (alphanum 1) (alphanum 42) ]
```
=> `[ "zero" "one" "I do not count beyond one" ]`

*/
switch-if = c: d: (findFirst (getAttr "cond") {} c).out or d;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use camelCase (switchIf).


/* Usage:
```nix
switch x [
simple-clause-1
..
simple-clause-k
] default-out
```
where a simple-clause has the form `{ case = p; out = r; }`
the first branch such as `p x` is true
or
```nix
switch [ x1 .. xn ] [
complex-clause-1
..
complex-clause-k
] default-out
```
where a complex-clause is either a simple-clause
or has the form { cases = [ p1 .. pn ]; out = r; }
in which case the first branch such as all `pi xi` are true

if the variables p are not functions,
they are converted to a `equal p`
if `out` is missing then `default-out` is taken
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a use case for a complex-clause without an out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have one somewhere, but anyway, this makes the code shorter!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it? I thought removing this would make this code:

    switch-if (map (cl: { cond = combine cl var;
                          out = cl.out or default; }) clauses) default;

become:

    switch-if (map (cl: cl // { cond = combine cl var; }) clauses) default;

But if you actually use this, never mind.


Example:
```nix
let test = v1: v2: with versions; switch [ v1 v2 ] [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what is the purpose of with versions; in this example. Did you originally intend to have a more realistic example, changed your mind, and forgot to remove with versions;?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahah yes this is deadcode. Well spotted!

{ cases = [ (x: x * 2 < 4) 8 ]; out = "small, 8"; }
{ case = [ 42 42 ]; } # no `out` means `default-out`
{ case = l: fold add 0 l > 42; out = "sum over 42"; }
{ case = [ 0 1 ]; out = "zero, one"; }
] "weird"; in
[ (test 1 8) (test 40 3) (test 0 1) (test 42 42) (test 1 0)]
```
=> `[ "small, 8" "sum over 42" "zero, one" "weird" "weird" ]`
*/
switch = var: clauses: default: with preds; let
Copy link
Member

@sternenseemann sternenseemann Jun 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only have switch, but the magic should be explicit by checking which of those attributes the case attribute set has:

  • cond: Is a predicate gets converted to all (equal true) (zipListsWith equal cl.conds var)`
  • conds:
  • case: gets converted to equal x
  • cases: gets converted to all (equal true) (zipListsWith equal cl.cases var)

Some additional ideas:

  • There should be a version of switch with var as its last argument, so it can be curried easily.
  • Having a type of condition for boolean expressions would also be nice (maybe named cond and the current renamed to pred?), so expressions like !lib.inNixShell could be incooperated nicely.
  • oneof: which would be roughly builtins.any (equal true) (builtins.map (equal x) cl.oneof) could be nice

Edit: Fixed my cases example, added conds.

Copy link
Contributor Author

@CohenCyril CohenCyril Jun 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sternenseemann Don't you like the fact that when some x is not a function it gets converted automatically to equal x? This looks harmless to me as it would not make sense to compare two functions, and it allows to combine both styles within cases (as in {cases = [somePred x]; ...} where somePred is really a predicate and x is not), which I think is a common usecase. Moreover it also frees cond for boolean conditions in order to subsume switchIf. I like your oneof suggestion and would also add allof for the sake of dualization.

Indeed switch is not flipable conveniently via flip... maybe we could have switchFun = cs: dft: x: switch x cs dflt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the “magic” is pretty cool, but I'm also a bit cautious in this regard. There is always the danger of for example a function that is partially applied by accident triggering the magic although you didn't want to — resulting in a weird and hard to debug eval error.

Not sure what the best call is here.

compare = f: if isFunction f then f else equal f;
combine = cl: var:
if cl?case then compare cl.case var
else all (equal true) (zipListsWith compare cl.cases var); in
switch-if (map (cl: { cond = combine cl var;
out = cl.out or default; }) clauses) default;
}
115 changes: 111 additions & 4 deletions lib/versions.nix
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
/* Version string functions. */
{ lib }:

rec {
with lib;
let
truncate = n: v: concatStringsSep "." (take n (splitVersion v));
opTruncate = op: v0: v: let n = length (splitVersion v0); in
op (truncate n v) (truncate n v0);
in rec {

/* Break a version string into its component parts.

Example:
splitVersion "1.2.3"
=> ["1" "2" "3"]

Remark: the builtins version of `splitVersion` -- which is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this isn't a Frenchism. I would use Note: instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the problem with "remark", except that it is an understatement. "Disclaimer" would be more appropriate IMO
...

considered first -- has a behaviour which is different from
`(lib.splitString ".")`. For example, it also recognizes `-` and
transitions between numbers and other strings as delimiters.
This behaviour is hence inconsistent between versions of nix and
should not be relied upon.

Example (discouraged):
splitVersion "1-2a+3"
=> [ "1" "2" "a+" "3" ]
*/
splitVersion = builtins.splitVersion or (lib.splitString ".");

Expand Down Expand Up @@ -35,15 +51,106 @@ rec {
*/
patch = v: builtins.elemAt (splitVersion v) 2;

/* Get string of the first n parts of a version string.

Example:
- truncate 2 "1.2.3-stuff"
=> "1.2"
- truncate 4 "1.2.3-stuff"
=> "1.2.3.stuff"
Comment on lines +59 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be?

Suggested change
- truncate 4 "1.2.3-stuff"
=> "1.2.3.stuff"
- truncate 3 "1.2.3-stuff"
=> "1.2.3-stuff"

Copy link
Contributor Author

@CohenCyril CohenCyril Feb 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the pitfall of this truncation function follows from the implementation of builtins.splitVersion, which has the following behaviour

builtins.splitVersion "1.2.3-stuff" == [ "1" "2" "3" "stuff" ]

In the same way the current (in nixpkgs master branch) majorMinor implementation satisfies the following:

lib.versions.majorMinor "1.2-stuff" == "1.2"
lib.versions.majorMinor "1-stuff" == "1.stuff"

I merely abstracted away the body of the function to generalize it to majorMinorPatch.

I agree this is not intuitive, but that's the best I can do that does not break compatibility and extends the current library quite uniformly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, maybe add a note on splitVersion in the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you should also add a note in the documentation of truncate that it doesn't preserve the delimiter type explicitly.

*/

inherit truncate;

/* Get string of the first two parts (major and minor)
of a version string.

Example:
majorMinor "1.2.3"
=> "1.2"
*/
majorMinor = v:
builtins.concatStringsSep "."
(lib.take 2 (splitVersion v));
majorMinor = truncate 2;

/* Get string of the first three parts (major, minor and patch)
of a version string.

Example:
majorMinorPatch "1.2.3-stuff"
=> "1.2.3"
*/
majorMinorPatch = truncate 3;

/* Version comparison predicates,
- isGe v0 v <-> v is greater or equal than v0 [*]
- isLe v0 v <-> v is lesser or equal than v0 [*]
- isGt v0 v <-> v is strictly greater than v0 [*]
- isLt v0 v <-> v is strictly lesser than v0 [*]
- isEq v0 v <-> v is equal to v0 [*]
- range low high v <-> v is between low and high [**]

[*] truncating v to the same number of digits as v0
[**] truncating v to low for the lower bound and high for the upper bound

Difference with `versionAtLeast` and `versionOlder`:
- `versionAtLeast` and `versionOlder` implement order relations on
version numbers, which are total assuming the only separator
which is used is `.`.

- `isGe`, `isLe`, `isLt`, and `isEq` are not order relations, they
are not antisymmetric (e.g. both `isLe "1.1" "1.1.2"` and `isLe
"1.1.2" "1.1"` are true).
`(isLe x) y` reads «`y` is a version which is lesser or equal to `x`».
Contrarily to `versionAtLeast`, it uses the first argument to
determine the length to truncate so that `isLe "1.1" "1.1.2"` is
true while `versionAtLeast "1.1" "1.1.2"` is false.

The main motivation is to be able to combine and read them nicely
with high order function such as:
- `filter`, e.g. the result of
```
filter (versionAtLeast "1.0")
[ "1.3.1" "1.0.0" "1.0" "0.9.0" "1.0.2" ]
```
is `[ "1.0" "0.9.0"]`.
In comparison
```
filter (isLe "1.0")
[ "1.3.1" "1.0.0" "1.0" "0.9.0" "1.0.2" ]`
```
returns `[ "1.0.0" "1.0" "0.9.0" "1.0.2" ]`, which can be read
naturally in English as «we keep versions that are lesser or
equal to "1.0"» and the outcome matches the expectations that
"1.0.0" and "1.0.2" belong to the family of "1.0" versions.
If you wanted to filter more precisely you should give the
patch version of the first argument of `isLe` instead (as in
`isLe "1.0.0"`, and if you wanted to exclude all "1.0", then you
should use `isLt "1.0"`.
- `switch` as in:
pkgs/development/coq-modules/mathcomp/default.nix#L21-L22

Examples:
- isGe "8.10" "8.10.1"
=> true
- isLe "8.10" "8.10.1"
=> true
- isGt "8.10" "8.10.1"
=> false
- isGt "8.10.0" "8.10.1"
=> true
- isEq "8.10" "8.10.1"
=> true
- range "8.10" "8.11" "8.11.1"
=> true
- range "8.10" "8.11+" "8.11.0"
=> false
- range "8.10" "8.11+" "8.11+beta1"
=> false
*/
isGe = opTruncate versionAtLeast;
isGt = opTruncate (flip versionOlder);
isLe = opTruncate (flip versionAtLeast);
isLt = opTruncate versionOlder;
isEq = opTruncate preds.equal;
range = low: high: preds.inter (versions.isGe low) (versions.isLe high);
Comment on lines +149 to +154
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the naming here pretty confusing and it is non obvious to a reader in another file that the version number gets truncated before the predicate is applied.

Also what is the exact motivaton for this versionAtLeast etc. can already deal with versions of different lengths.

Copy link
Contributor Author

@CohenCyril CohenCyril Feb 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The major difference is that versionAtLeast implements an order relation on version numbers (which is total assuming the only separator which is used is .), while the predicates I implement here are not order relations, they are not antisymmetric (in the sense that both isLe "1.1" "1.1.2" and isLe "1.1.2" "1.1" are true) and (isLe x) y should be read as «y is a version which is lower or equal to x». Contrarily to versionAtLeast, its analogous isLe uses its first argument to determine the length to truncate so that isLe "1.1" "1.1.2" is true while versionAtLeast "1.1" "1.1.2" is false...

The main motivation is to be able to combine and read them nicely with high order function such as

  • filter, e.g. the result of filter (versionAtLeast "1.0") [ "1.3.1" "1.0.0" "1.0" "0.9.0" "1.0.2" ], which is [ "1.0" "0.9.0" ] is very unnatural to me, both in reading versionAtLeast the wrong way around and the fact that I would like 1.0.2 to be included in the output: indeed "1.0.2" is part of the family of "1.0" versions! In comparison filter (isLe "1.0") [ "1.3.1" "1.0.0" "1.0" "0.9.0" "1.0.2" ] reads more naturally (we keep versions that are lesser or equal to "1.0") and the outcome [ "1.0.0" "1.0" "0.9.0" "1.0.2" ] matches my expectations: if I wanted to filter more precisely I would give the patch version of the first argument of isLe instead, and if I wanted to exclude all "1.0", then I would use isLt "1.0".
  • switch (defined in this PR), for example
    defaultVersion = with versions; switch coq.coq-version [
    { case = isGe "8.13"; out = "1.12.0"; } # lower version of coq to 8.10 when all mathcomp packages are ported

EDIT: I corrected a few typos that made it difficult to understand the first paragraph... should be better now.

Copy link
Contributor Author

@CohenCyril CohenCyril Feb 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this should be explained in a better way inside the comment in the code. I will try my best to be clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a new commit with better explanations (basically what is up here modulo transformation from github PR comment to inline code comment).


}
2 changes: 0 additions & 2 deletions pkgs/applications/science/logic/coq/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
, csdp ? null
, version, coq-version ? null,
}@args:
let lib' = lib; in
let lib = import ../../../../build-support/coq/extra-lib.nix {lib = lib';}; in
with builtins; with lib;
let
release = {
Expand Down
3 changes: 1 addition & 2 deletions pkgs/build-support/coq/default.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{ lib, stdenv, coqPackages, coq, fetchzip }@args:
let lib = import ./extra-lib.nix {inherit (args) lib;}; in
with builtins; with lib;
let
isGitHubDomain = d: match "^github.*" d != null;
Expand Down Expand Up @@ -74,7 +73,7 @@ stdenv.mkDerivation (removeAttrs ({

meta = ({ platforms = coq.meta.platforms; } //
(switch domain [{
case = pred.union isGitHubDomain isGitLabDomain;
case = preds.union isGitHubDomain isGitLabDomain;
out = { homepage = "https://${domain}/${owner}/${repo}"; };
}] {}) //
optionalAttrs (fetched.broken or false) { coqFilter = true; broken = true; }) //
Expand Down
Loading