at a high level, we want to be able to rebind stuff other than just literals.
Say for example, we have some trading strat written in rainlang that has a ratio that represents a multiplier that we apply to our price and/or amount, maybe to respond to some kind of trend in the market or something.
A reasonable workflow is something like:
Define a binding in the dotrain file for the ratio, elided with a nice description
Write the strat that uses the binding for all its internal logic
Do some kind of monte carlo analysis over the ratio to make nice simulations and charts, by rebinding and evaling locally
To go to production, rebind the ratio to the logic that will calculate the real ratios onchain, e.g. it could pull from a uni v3 twap or similar
Steps [0-2] all work great already, we can simulate things with native rainlang using local evm environments, without the need for complex test harness logic like needing to mock underlying contracts.
This is a very haskell-like approach, lifting everything that is impure to the entrypoint, we don't formally enforce it anywhere but it's a simple pattern that allows dotrain composition to also be a native replacement for the need to mock externalities.
In the future we might have a rainlang native alternative - https://github.com/rainlanguage/rain.interpreter/issues/137 - but for now the pattern of rebinding and composing is totally workable and better than needing to use foundry/hardhat for mocking or modelling in separate systems (e.g. python). Remember that the ultimate goal is that everyone can do everything they need to do by only learning rainlang and using rainlang native tooling.
Unfortunately step 3. doesn't actually work currently, in dotrain you can't rebind #ratio !the ratio to #ratio uniswap-v3-twap-output-ratio( ... ) because non-literal bindings are not supported.
The "obvious solution" and why it is not so obvious
Ok so we can just allow binding to RHS items, right?
Actually this is what I was thinking we'd do for a while, but thinking about it more (and realising a potential alternative, detailed below), it's problematic.
Macrology is subtle to implement and often hard to use
If we are rebinding things that reference other things then the lexical nature of these references is important.
Consider some binding:
---
#foo add(a b)
What exactly are a and b? is it expected that there exists some defined a and b already in scope wherever the binding is realised? If so, doesn't this make composition and re-use quite difficult, having to match the names of everything everywhere? If not, where do they come from?
Multi-output RHS items are awkward to bind
Consider some dotrain with hypothetical RHS binding:
---
#foo word-with-two-outputs(1)
#bar
a b: foo;
This is subjective, but it's kinda awkward to read imo.
Worse, the rebinding of literals for the sake of testing would have to match the outputs.
---
#foo 1 2
#bar
a b: foo;
or in cli form --bind 'foo=1 2'
It's weird now because a single "thing" in the rhs as foo is actually two different literal values once the binding resolves.
Alternative approach
What we could do is allow quotes to be bound, which might give us just enough indirection to do what we want (i.e. dev/prod parity with a pure rebinding based workflow).
Consider the following in the current state of dotrain, to test then deploy a simple ratio value.
---
#ratio !The ratio that main will return
#main-test
_: ratio;
#main
_: call<'the-real-ratio-logic 1>();
#the-real-ratio-logic
...
We need (at least) two different entrypoints, #main-test and #main so that we can use the rebindable version of the code, and the version of the code that has the production ratio logic in it. Presumably #the-real-ratio-logic itself is tested somehow, for the sake of the example we're just looking at the relationship between rebinding and shipping to production.
Here is how the same thing above could look if we could rebind quotes
---
#test-ratio !The ratio that main will return.
#ratio-binding !The binding to use to calculate the ratio.
#main
_: call<ratio-binding 1>();
#always-test-ratio
_: ratio;
#real-ratio-logic
...
In this case, we replace the need to ship a different entrypoint to production with the ability to ship the same entrypoint with a different rebinding. Specifically, we rebind the call in #main to either 'always-test-ratio or 'real-ratio-logic
In yaml form this would be a test case (I think the single quote needs escaping in yaml):
bind:
- test-ratio: 5e18
- ratio-binding \'always-test-ratio
---
#test-ratio !The ratio that main will return.
#ratio-binding !The binding to use to calculate the ratio.
#main
_: call<ratio-binding 1>();
#always-test-ratio
_: ratio;
#real-ratio-logic
...
Then the production deployment looks like:
bind:
- ratio-binding \'real-ratio-logic
---
#test-ratio !The ratio that main will return.
#ratio-binding !The binding to use to calculate the ratio.
#main
_: call<ratio-binding 1>();
#always-test-ratio
_: test-ratio;
#real-ratio-logic
...
Note that the strat itself doesn't need to change, nor do we need redundant entrypoint definitions, we just modify the quote that ends up in the call in the main entrypoint, to change the subsequent dispatch.
This avoids the issues with RHS item binding because:
Quotes reference bindings, which themselves can only be entire expressions, or literals, so there's no lexical issues with references
Pragmatically quotes are primarily for enabling call and this already has an established convention and implementation for matching inputs and outputs, so a single "thing" in the binding will map to however many inputs and outputs it needs to be
It's easy to see how this enables composition and extension if someone wants to invent a new way to calculate a ratio, it can be bound to something that is compatible with the same call and then simply rebound into #main if/when needed
Things NOT in scope
It seems obvious that a shallow implementation of the above should be relatively easy to implement. We substitute the binding for the quote, then resolve quotes.
As long as this substitution happens before quote resolution, everything works as normal.
A deep implementation that requires resolving quotes in order to substitute them could be much more complex, and it's not needed.
motivation
So previously we discussed wanting to be able to (re)bind RHS items with dotrain
actually there's even a poorly defined idea of a "rainlang fragment" in the dotrain spec https://github.com/rainlanguage/specs/blob/main/dotrain.md#rainlang-fragment
at a high level, we want to be able to rebind stuff other than just literals.
Say for example, we have some trading strat written in rainlang that has a ratio that represents a multiplier that we apply to our price and/or amount, maybe to respond to some kind of trend in the market or something.
A reasonable workflow is something like:
Steps
[0-2]
all work great already, we can simulate things with native rainlang using local evm environments, without the need for complex test harness logic like needing to mock underlying contracts.This is a very haskell-like approach, lifting everything that is impure to the entrypoint, we don't formally enforce it anywhere but it's a simple pattern that allows dotrain composition to also be a native replacement for the need to mock externalities.
In the future we might have a rainlang native alternative - https://github.com/rainlanguage/rain.interpreter/issues/137 - but for now the pattern of rebinding and composing is totally workable and better than needing to use foundry/hardhat for mocking or modelling in separate systems (e.g. python). Remember that the ultimate goal is that everyone can do everything they need to do by only learning rainlang and using rainlang native tooling.
Unfortunately step 3. doesn't actually work currently, in dotrain you can't rebind
#ratio !the ratio
to#ratio uniswap-v3-twap-output-ratio( ... )
because non-literal bindings are not supported.The "obvious solution" and why it is not so obvious
Ok so we can just allow binding to RHS items, right?
Actually this is what I was thinking we'd do for a while, but thinking about it more (and realising a potential alternative, detailed below), it's problematic.
Macrology is subtle to implement and often hard to use
Basically this suggestion introduces some gnarly problems native to macros in other languages, such as macro hygiene https://en.wikipedia.org/wiki/Hygienic_macro
If we are rebinding things that reference other things then the lexical nature of these references is important.
Consider some binding:
What exactly are
a
andb
? is it expected that there exists some defineda
andb
already in scope wherever the binding is realised? If so, doesn't this make composition and re-use quite difficult, having to match the names of everything everywhere? If not, where do they come from?Multi-output RHS items are awkward to bind
Consider some dotrain with hypothetical RHS binding:
This is subjective, but it's kinda awkward to read imo.
Worse, the rebinding of literals for the sake of testing would have to match the outputs.
or in cli form
--bind 'foo=1 2'
It's weird now because a single "thing" in the rhs as
foo
is actually two different literal values once the binding resolves.Alternative approach
What we could do is allow quotes to be bound, which might give us just enough indirection to do what we want (i.e. dev/prod parity with a pure rebinding based workflow).
Consider the following in the current state of dotrain, to test then deploy a simple ratio value.
We need (at least) two different entrypoints,
#main-test
and#main
so that we can use the rebindable version of the code, and the version of the code that has the production ratio logic in it. Presumably#the-real-ratio-logic
itself is tested somehow, for the sake of the example we're just looking at the relationship between rebinding and shipping to production.Here is how the same thing above could look if we could rebind quotes
In this case, we replace the need to ship a different entrypoint to production with the ability to ship the same entrypoint with a different rebinding. Specifically, we rebind the call in
#main
to either'always-test-ratio
or'real-ratio-logic
In yaml form this would be a test case (I think the single quote needs escaping in yaml):
Then the production deployment looks like:
Note that the strat itself doesn't need to change, nor do we need redundant entrypoint definitions, we just modify the quote that ends up in the
call
in the main entrypoint, to change the subsequent dispatch.This avoids the issues with RHS item binding because:
call
and this already has an established convention and implementation for matching inputs and outputs, so a single "thing" in the binding will map to however many inputs and outputs it needs to becall
and then simply rebound into#main
if/when neededThings NOT in scope
It seems obvious that a shallow implementation of the above should be relatively easy to implement. We substitute the binding for the quote, then resolve quotes.
As long as this substitution happens before quote resolution, everything works as normal.
A deep implementation that requires resolving quotes in order to substitute them could be much more complex, and it's not needed.
Let's keep
out of scope