merklejerk/solpp

Lambdas?

Closed this issue · 3 comments

One pain point I have in Solidity is the lack of lambdas or coroutines. If I need to set up some complicated iteration of a data structure many times in the code, I achieve this with macros currently like so:

// #def ITER_THINGS(...) \
//           for (uint i=0; ...) {     Thing thing = things[i];

// #def END_ITER_THINGS() \
//           }

And in the code:

$(ITER_THINGS(someArgs))
doStuffWith(thing);               // `thing` variable implicitly available
$(END_ITER_THINGS())

The objective isnt to repeat the code, it's to separate out the complexity of the traversal with whatever mapping function you want to implement.

Is there a way to achieve this? I guess you could make a list of all the things you want to iterate and then loop over that, but it seems like it might not be the most efficient way.

Solidity actually supports function pointer types natively, so you could do something like:

pragma solidity ^0.8;

contract Foo {
    function bar(uint256[] memory values) public pure returns (uint256[] memory) {
        return _map(values, _timesTwo);
    }
    
    function _map(
        uint256[] memory values,
        function (uint256) pure returns (uint256) transformer
    )
        internal
        pure
        returns (uint256[] memory results)
    {
        results = new uint256[](values.length);
        for (uint256 i = 0; i < values.length; ++i) {
            results[i] = transformer(values[i]);
        }
    }
    
    function _timesTwo(uint256 v) internal pure returns (uint256 r) {
        r = v * 2;
    }
}

I didn't know that, so that's good to know... But, I think proper lambdas could be neat...

Consider that in my example above I can make a loop over any complex iteration I want. It allows me to separate out the iterator completely, so I could use it for a map or a fold or anything, without additional intermediary data structures or overheads. The mapper in your example is still limited to providing the results as a list, which might not always be what you want.

Also, in my example above I can pass arguments neatly in but the arguments it generates are implicit.

What I want to be able to write is something like:

// #defmap ITER_THINGS(someArg) \
//           for (uint i=0; ...) { $apply(things[i]) }

$(ITER_THINGS(someArg) | (thing) => {
    .. function body
})

It also gets the benefit of sharing the same context 🙂 .

Going to close this; my experience of using solpp for iterators was always a bit cumbersome, in the end it was cleaner to encapsulate the iterator state in a type specific to the iterator, and the logic in functions, ie:

struct MyIter {
  ...
}

function myIterLength(MyIter memory mi) returns (uint) { ... }
function myIterNext(uint i) returns (MyItem) { ... }

Bit of an old school C way of doing things but cleaner than using preprocessor in this case 🙂