MatrixAI / Architect

Programming Language for Type Safe Composition of Distributed Infrastructure
Apache License 2.0
1 stars 0 forks source link

Fixed point "Dynamic Binding" that allows overrides across the configuration graph #22

Open CMCDragonkai opened 5 years ago

CMCDragonkai commented 5 years ago

This could be applied to the Architect language as well: http://r6.ca/blog/20140422T142911Z.html

Automatons that require their late bound dependencies to change can be considered to be a form of dynamic binding. Static binding is basically the same as constructing the Automaton requiring instances of the dependent automatons to be supplied. Whereas dynamic binding allows you to change what the depending Automatons are.

Automaton is an overloaded word. There's a concept of an Automaton that exists in the Architect language, which is a declarative functional typed language. And there's a concept of an Automaton that exists in Emergence, which is a running instance of an artifact.

In Architect, there is a notion of Automaton as a declarative specification, and there's a notion of Automaton instance that represents a running instance. In the Architect language we are not meant to directly manipulate instances, because doing so is stateful and imperative. Instead we should be declaratively composing Automaton specifications and the instances are automatically derived.

However the TUI/GUI may later require imperative interrogation of the Automaton instances, and in that case there is always some representation of the Automaton instance that is being interrogated. It is always a representation, because the operators operating on the TUI/GUI do not possess the Automaton instance, the Automaton instance is always some stateful thing that exists remotely.

CMCDragonkai commented 5 years ago

I didn't fully understand the usage of fix for both anonymously recursive functions but also productive data structures using constructors until I read this: https://elvishjerricco.github.io/2017/08/22/monadfix-is-time-travel.html

The introduction shows an interesting case of:

> fix $ \(~(a, b, c)) -> ([1, c-5], head a + 2, b * 4)
([1,7],3,12)

This is a function that takes a tuple and returns a tuple. It just so happens that the expectation is that the input tuple IS the output tuple. To do this, you need to apply fix to the function. Where fix :: (a -> a) -> a. Swap tuples for attribute sets in Nix, and that's basically how rec sets are done.

To make this all work you need lazy evaluation. The ~ on the pattern match is syntax sugar to do "lazy pattern matching". Basically instead of really pattern matching, it's preserving the entire structure, and instead replacing each component with record access. In the case of tuples, that's basically fst, snd.. etc.

This is important because if since fix f = let x = f x in x, a strict pattern match on the input would mean the x is going to be infinitely expanded f (f (f (f (...))).

CMCDragonkai commented 5 years ago

This article: https://github.com/ElvishJerricco/extensible-config takes the fix and later mfix to encode the extensible configuration concept in Haskell. I believe hnix probably has something similar.

Basically we start with a record with properties that we then turn into lens so we can access it via lenses. Then they create endomorphisms, functions that take the a type a and return the a. And these functions are composed. However they need to be wrapped up as a monadic operation. So at the end, each override operation is a:

newtype EndoM m a = EndoM { appEndoM :: a -> m a }
type Configurable a = EndoM (Reader a) a
override :: Configurable MyConfig

Should compare this against the hnix implementation of attribute sets and the ability to override them. Note that doing such a thing is different from Nix style rec bindings.

The very first article shows that sometimes we want to "update" an attribute set, and other times we want to "override" an attribute set. The difference is that, updating means adding or replacing an attribute on the final attribute set representation. Whereas override appears to to affect every other attribute that relies on the attribute that you are changing. Thus the first is a sort of "lexically bound" update. Whereas the second is a sort of "dynamically bound" update.

These 2 usecases appear in quite a few places. In Nix, packages often rely on other packages. Unlike a strict hierarchy, all packages are defined on a flat hierarchy. In order for a package definition to rely on another package definition, they must be functions that accept them. And something else must pass the other package definitions in. Sometimes when you're using a set of packages as dependencies for your system, you need to change something about a package. However you cannot just update the package set trivially, because this doesn't change how all the other packages make use of that package. It would only update for any downstream consumers of that package that you just updated. So you need an "override" instead, that ensures that all other packages that depend on that package also receives the new package. In a way, dynamic binding is a sort of mutation. But such a mutation is useful when dealing with large cyclic structures such as package graphs.

In Matrix we also have something similar with Automatons depending on other Automatons. We would often want to "update" an Automaton, but what we are really doing is "overriding" that Automaton in a dynamic binding sense.

This example in http://r6.ca/blog/20140422T142911Z.html shows this quite clearly:

let fix = f: let fixpoint = f fixpoint; in fixpoint;
       withOverride = overrides: f: self: f self // overrides;
       virtual = f: fix f // { _override = overrides: virtual (withOverride overrides f); }; in
   let a = virtual (self: with self; { x = "abc"; x2 = x + "123"; }); in rec
   { example1 = a;
     example2 = a // { x = "def"; };
     example3 = a._override { x = "def"; };
     example4 = example3._override { y = true; };
     example5 = example4._override { x = "ghi"; };
   }

{ example1 = { _override = <LAMBDA>; x = "abc"; x2 = "abc123"; };
  example2 = { _override = <LAMBDA>; x = "def"; x2 = "abc123"; };
  example3 = { _override = <LAMBDA>; x = "def"; x2 = "def123"; };
  example4 = { _override = <LAMBDA>; x = "def"; x2 = "def123"; y = true; };
  example5 = { _override = <LAMBDA>; x = "ghi"; x2 = "ghi123"; y = true; };
}

With the usage of // as simply updating. But with the addition of _override to allow "overriding" attributes instead.

CMCDragonkai commented 5 years ago

This kind of cyclic structure is called "open recursion". In a type theory sense. Static binding is "closed recursion" with the "knot" being tied in the introduction form. Whereas the dynamic binding is "open recursion" with the "knot" being tied in the elimination form.

Static binding is still good because it allows us to normalise the expressions and reduce them to some simple form. It's part of referential transparency.

CMCDragonkai commented 5 years ago

Another little cool thing you can do:

> let f = { a ? 1, b ? a }: a + b; in f {}
2

The attribute sets at the function head are automatically recursive attribute sets. You don't even need a let rec.

Haskell doesn't even have this ability because you cannot define default parameters at the function head. Instead the client must pass in a "fixed" data structure to do this. Although you have the def from Data.Default.

Here we are actually writing out a function that will take some arbitrary attribute set, then if doing a runtime type check on the structure, unless there's a ... at the very end. Then merging our own attribute set declared at the head into the input attribute set. It's quite sophisticated machinery, and the equivalent in Haskell would have to be some sort of syntax sugar.