haskell-nix / hnix

A Haskell re-implementation of the Nix expression language
https://hackage.haskell.org/package/hnix
BSD 3-Clause "New" or "Revised" License
753 stars 115 forks source link

Use a binding library to implement scopes in Eval.hs #106

Open jwiegley opened 6 years ago

jwiegley commented 6 years ago

Right now we do name lookups in a hash maps, which means that for x: x + x + x, there will be three lookups of the same argument. With unbound-generics we can perform the substitution by using a subst function on expressions, which will probably mean introducing two phases of evaluation: one that uses an F-algebra NExprF NExpr -> NExpr simply to reduce the underlying lambda calculus by performing binds and applications, and then the current evaluator for performing the remaining computations.

jwiegley commented 6 years ago

A nice introductory article to the concept: https://byorgey.wordpress.com/2011/03/28/binders-unbound/

The reimplementation of that idea using GHC Generics: http://hackage.haskell.org/package/unbound-generics

Pinging @lambdageek, whose presence is missed when Theo and I are discussing lambda calculii. :)

lambdageek commented 6 years ago
lambdageek commented 6 years ago

Also if your binding situation is simple, I wouldn't discount bound. It's pretty great. Especially if you love polymorphic recursion and you only have a single sort of variable.

In a lot of ways unbound-generics is more appropriate for languages with exotic binding structure:

jwiegley commented 6 years ago

@lambdageek Is there any performance difference between them? Will bound let us easily handle recursive bindings?

There are also cases in Nix where you can take an "attribute set" (basically just a Map), and convert the value into a binding scope using a with keyword. I'm wondering how to do that with these binding libraries. Here's an example:

let f = pkgs: 
    let y = 10; 
    in
        with pkgs; 
            [ y x ]; 
in
    f { x = 100; }

What Nix requires here is that the attribute set that's passed as an argument to f, and becomes the value bound to pkgs for that call, is non-strictly transformed into the dynamic scope made visible by with only if lookup fails, meaning that when the lookup of y is done, pkgs is left unevaluated, but when the lookup for x is performed and fails, then and only then does pkgs get forced in order to establish a scope in which x can be resolved.

lambdageek commented 6 years ago

@jwiegley

jwiegley commented 6 years ago

I’m no longer convinced that this issue is necessary, for two reason: We want NExpr to be a “raw” expression tree, for purposes of pretty printing, where it makes no sense to do name resolution. And due to the dynamic nature of Nix, some name lookups will have to be deferred until evaluation anyway. Bound would give us faster substitution when calling into lambdas, but we have the case of attribute sets to deal with, where we can’t use Nats for indexing.

So I’m closing until a compelling reason arises.

jwiegley commented 6 years ago

Nope, it's necessary, otherwise we re-evaluate expensive function bodies with each new set of arguments.

Ericson2314 commented 5 years ago

I saw https://github.com/clash-lang/clash-compiler/pull/361 which is disappointing. I desperately want one of these libraries to win, and end the duplication we have today.

christiaanb commented 5 years ago

@Ericson2314 Clash might've been one of the worst use-cases with regards to performance of unbound because it does sooooo many traversals of an expression, going under and over binders all the time.

Given that all of my options to improve Clash' run-time situation basically meant I had to (probably) change nearly every module in the compiler, I went with the option that seemed to be the safest performance-wise: implement the system GHC uses. My particular use case was:

Doing my own implementation has been causing loads of shadowing/capture/free variable issues though! And implementing it has taken me over 5 weeks now; and even though it passes the test suite, I'm quite sure there are still bugs.

So if you're not constantly/repeatedly traversing deeply-nested binding structures, I don't think there's much of a performance penalty when using unbound-generics; I would definitely recommend it.