From Stack Machine to Functional Machine: Step 2 — Currying

Loredana Cirstea
Coinmonks
7 min readApr 28, 2020

--

Read on Github.

tags: Taylor, Ethereum, Solidity, Yul, eWasm, WebAssembly

This is a gradual introduction to my talk at the Solidity Summit, Wednesday, 29th of April at 2:50:00 PM CEST. Agenda.

Environment

For illustrating our journey, we will use the Yul language (that compiles to Ethereum 1.0 and Wasm bytecode).

If you want to run the examples, it can be done with https://remix.ethereum.org:

  • choose Yul as the compiled language, use the raw calldata input, check the return value using the debugger.
  • use the Yul+ plugin to compile, deploy and interact (you will need to comment out the mslice helper function)

The code example below can also be found at https://gist.github.com/loredanacirstea/1aa18e33342b862d8dc76c01b12b7dbc.

Prerequisites

Read the previous article From Stack Machine to Functional Machine: Step 1 (recursive apply).

Currying

Currying is the technique of breaking down a function that takes multiple arguments into a series of functions, each taking one or more of those arguments.

Therefore, we can write const sum = (a, b) => a + b as:

And now we can reuse sumPartial in other places in our code, for example, as an argument to a map function: map(array, sumPartial).

In our on-chain interpreted type system Taylor, with currying, we can define classes of types. uint itself is a partially applied function and now we can reuse this function as uint(256) and we will get a concrete type.

Elastic Arity

Currying and de-currying are important tools to achieve better human-computer communication.

If the human is used to a sum function of arity 2: sum(a, b), by currying, the computer will interpret it as a composition of functions with arity 1: sum(a)(b)

If there is a family of functions of arity n, a covering function of arity n+1 can be constructed such that any one of the initial functions are called by means of an additional argument that does the selection.
Having the arity dynamic may make the function much more intuitive:

Currying in the Ethereum Virtual Machine and WASM

Yul allows us to work directly with the stack and memory, so we have enough freedom to implement a currying system at runtime.

All that we need to do is maintain a space in memory, where our curried functions reside. In the following code, we will treat each memory pointer to a curried function as the curried function’s signature.

At the memory pointer, we will find the signature of the underlying function, along with the partially-applied arguments. In our above example, this would mean <sumCurried_signature>0000000000000000000000000000000000000000000000000000000000000040 ( 64 = 0x40).

Now, we can use the curried function’s signature in other functions and we are going to build upon the recursive apply code presented in our Step 1 article.

The following code allows us to recursively apply a series of functions, where the output of each function is fed to the next function, as input.

We have:

  • some “native” functions in executeNative, such as sum (0xeeeeeeee), recursiveApply(0xcccccccc) and curry (0xbbbbbbbb). And we will call recursiveApply with a number of steps, each step is a function that has some inputs.
  • executeCurriedFunction, which knows how to process curried functions
  • executeInternal, which knows how to distinguish a "native" from a curried function.

Currying Example: sum(64, 32)

The calldata will be: 0xffffffffcccccccc000000020000002800000020bbbbbbbbeeeeeeee00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020

Program Flow

Start call

  • the execute function calls recursiveApply with 000000020000002800000020bbbbbbbbeeeeeeee00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020
  • recursiveApply breaks down the steps and runs each of them, feeding the output of each step into the next one

Step 1

  • recursiveApply calls executeInternal with bbbbbbbbeeeeeeee0000000000000000000000000000000000000000000000000000000000000040
  • executeInternal sees that the signature is 4 bytes and calls executeNative, forwarding all data
  • executeNative unpacks the 0xbbbbbbbb signature and the program reaches the curry function.
  • curry stores the virtual, curried function signature 0xeeeeeeee and the partially applied argument 0x0000000000000000000000000000000000000000000000000000000000000040 (64) at a memory pointer and writes that pointer into the output_ptr
  • the program returns to recursiveApply, which prepares the output as input for the next step

Step2

  • recursiveApply calls executeInternal with <sumPartial_pointer>0000000000000000000000000000000000000000000000000000000000000020
  • executeInternal sees that the signature is 32 bytes and calls executeCurried, forwarding all data
  • executeCurried calls executeInternal with 0xeeeeeeee0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000020, merging the curried function data with the new input
  • executeInternal sees that the signature is 4 bytes and calls executeNative
  • executeNative unpacks the 0xeeeeeeee signature and the program reaches the sum function
  • sum adds the two arguments and writes the answer in the output_ptr memory pointer.
  • the program returns to recursiveApply and output_ptr points at the result

Return

  • the program returns to execute and the result from output_ptr is returned

Having a technique for currying functions (at runtime) is the second step in turning a stack machine into a functional machine.

Partially applied functions can be very important when used as a map or reduce argument, allowing you to write extensible code.

Next: Step 3

In the next step, we will show you how higher-order functions can be used in this recursive engine.

Read step 3: From Stack Machine to Functional Machine: Step 3 (Higher Order Functions)

--

--