Functional Programming for Ethereum

Topics

  1. Introduction
  2. The State of FP in Ethereum
  3. Simplify Your Life
  4. Future Work

Introduction

What do we mean FP?

  • There are probably lots of definitions of Functional Programming. 

 

  • I could say we want to focus on purely functional languages with strong static typing, sounds complicated.

 

  • In this talk, it means languages which enable/encourage:
    • purity : no mutable state, managing effects
    • type systems : more than int ≠ bool

    • compiler-assistance : e.g. metaprogramming

What we're contrasting against

  • Javascript and golang are the de facto languages in this ecosystem. People seem to be very productive.

 

  • Neither can really be described as particularly future thinking or innovative as a programming language. [1]
     
  • Neither offers much in terms of safety or the ability to be statically analyzed for performance or behavior.

What we're advocating for

  • This talk is particularly promoting Haskell like languages for blockchain development. This means Haskell and PureScript. (Many benefits can also be found in Rust.)

 

  • Embracing the benefits of static analysis, sane abstraction, and code reuse.

 

  • Letting compilers do the job that compilers are good at so you can do your job.

Why this is important (today)

  • When we deploy our application that touches mainnet, databases, frontend and servers, we want to make sure that we don’t break anything.
     
  • Generated code has types, which means that we can not only encode what interacts with what, but how they interact.
     
  • Eliminates a whole set of tests (not all). The compiler performs these tests when doing type-checking.

Why this is important (today)

 

  • Our API is a type. So our whole API is specified through our (compiler-generated) swagger definition.
     
  • Instead of writing documentation that tells the developer how to interact with your API, you write types that tells the compiler how to interact with your API.

Why this is important (examples)

Why this is important (future)

  • One of our biggest limitations is the EVM and the lack of languages that target it.

 

  • We have solidity. It's notoriously difficult to write large applications, provides almost no safety features.

 

  • We're moving toward the era of application specific blockchains (substrate, cosmos) where language choice is open. Let's not mess it up this time.

What the &#%$! is PureScript?

  • PureScript is an opinionated dialect of Haskell created in 2013 (compare with TS 2012 , Elm 2012) 

 

  • Simplifies and distills concepts learned from Haskell's 30 year history (yes, it's older than Java) 

 

  • Supports multiple backends, Javascript being the most developed. (erlang, c++, golang)

 

  • Primary applications in UI development, but has mature libraries in many other domains. 

 

The State of FP in Ethereum

Libraries and Frameworks

PureScript

  • purescript-eth-core : core types, signature schemas, RLP encoding.
  • purescript-web3 : abi codecs, contract interactions, web3 api bindings
  • chanterelle : smart contract build tool, manages deployments, FFI generation, testing

Haskell

  • ​​hs-web3 : similar to purescript-web3, support for account managament and using solc.

FOAM

Core Contrib

History

Oct

Nov

2017

2018

Mar

May

1

2

3

  1. Decision to write Spatial Index (Beta) in PureScript.
  2. Decision to write native web3 library from scratch.
  3. Release feature complete version of purescript-web3
  4. Truffle migraine 🤕 . Decision to write replacement.
  5. Initial release of Chanterelle, public release of S.I. Beta

4

Dec

Apr

5

...

Present Day

  • All libraries used in launching, maintaining, and improving FOAM's mainnet application and others in production
     
  • Many new features and internal simplifications / refactors leading up to Devcon

 

  • With some exceptions there is no new work planned (e.g. EthPM support in chanterelle, Vyper support)

 

  • Largely not contributing to hs-web3. We run our own fork, so do others.

Comparing Stacks

Simplify Your Life

For some definition of the word simplify

Purity

  • Purity  means there is a separation in the type system between effectful code versus pure functions.

 

Examples of Effects

  1. Codecs
  2. Throwing an exception / indicating an error
  3. Invoking a web3 or other network call

Examples of Pure Functions

  1. Data transformations that can't fail
  2. Mathematical functions / operations

Purity (basic)

Public / Private Keys in Ethereum:

  • Ethereum's schema uses ECDSA on  secp256k1. 
  • A Private key determines a unique Public key.
  • An Address is the last 20 bytes of the hash of the Public key. 
import Data.ByteString as BS
import Network.Ethereum.Core.HexString

-- | Opaque PrivateKey type
newtype PrivateKey = PrivateKey BS.ByteString

-- | Opaque PublicKey type
newtype PublicKey = PublicKey BS.ByteString

-- | Represents and Ethereum address, which is a 20 byte `HexString`
newtype Address = Address HexString

Purity (basic)

unPublicKey :: PublicKey -> HexString

mkPublicKey :: HexString -> Maybe PublicKey

unPrivateKey :: PrivateKey -> HexString

mkPrivateKey :: HexString -> Maybe PrivateKey

-- | Produce the `PublicKey` for the corresponding `PrivateKey`.
foreign import privateToPublic :: PrivateKey -> PublicKey

unAddress :: Address -> HexString

mkAddress :: HexString -> Maybe Address

-- | Produce the `Address` corresponding to the `PrivateKey`.
privateToAddress :: PrivateKey -> Address

-- | Produce the `Address` corresponding to the `PublicKey`
publicToAddress :: PublicKey -> Address

Effects (basic)

class ABIEncode a where
  toDataBuilder :: a -> HexString

-- | type Parser String a = ExceptT ParseError (State (ParseState HexString)) a
class ABIDecode a where
  fromDataParser :: Parser HexString a
  
-- | Parse encoded value, droping the leading `0x`
fromData :: forall a . ABIDecode a => HexString -> Either ParseError a
fromData s = runParser s fromDataParser
  • Parser HexString a is a parser consuming a HexString to produce a value of type a.
  • It can update its stream of hex chars as the parser runs.
  • It can throw errors of type ParseError.
  • The parser can be run using runData to resolve the effects.

Effects (basic)

instance abiDecodeAddress :: ABIDecode Address where
    fromDataParser = do
      _ <- take 24
      addressBytes <- take 40
      case mkAddress addressBytes of
        Nothing -> fail "Address is 20 bytes, receieved more"
        Just addr -> addr
instance abiDecodeVector 
  :: ( ABIDecode a
     , KnownSize n
     ) 
  => ABIDecode (Vector n a) where
    fromDataParser = 
      let len = sizeVal (DLProxy :: DLProxy n)
      in replicateA len fromDataParser
  • The more information you put in your types, the less you have to rely on error effects to guard against invalid states

Effects (basic)

myAddresses = Either ParseError (Vector (DLProxy D2) Address)
myAddresses = 
  fromData "0x0000000000000000000000000000000000000000000000000000000000000002
              0000000000000000000000003a9bCa3065b263046CEf072210cdb5845B05f1A3
              0000000000000000000000001eA6e6eCDe9A6B8229Ebf73e391b16dd63fc038B"

secondAddress :: Either String Address
secondAddress = case myAddresses of
  -- in case of error print a nice message
  Left parseError -> Left ("Error parsing myAddresses: " <> show parseError)
  -- in case of success, grab the address at index 1, guaranteed to succeed.
  Right addresses -> addresses !! (DProxy :: DProxy D1)

How this might be useful

Effects (advanced)

  • Any computation that touches the "real" world via I/O is effectful, especially web3 calls.
-- Web3 is a context that has access to a web3 provider and
-- can make aysynchronous computations. It can also throw
-- excptions via Aff.
newtype Web3 a = Web3 (ReaderT Provider Aff a)

...

-- | Call a function on a particular block's state root.
eth_call :: TransactionOptions NoPay -> ChainCursor -> Web3 HexString
...
  • There is a big difference between a value of type a and Web3 a.
     
  • Understanding this difference, or at least getting used to it, is the basis of this kind of FP.

Effects (advanced)

  ...
  let 
     {contractAddress: mockERC20Address, userAddress} = cfg
     
     -- number of tokens to transfer
     amount = mkUIntN s256 1
     
     recipient = nullAddress
     
     -- set the `to` and `from fields for the transaction options
     txOptions = defaultTestTxOptions # _from ?~ userAddress
                                      # _to ?~ mockERC20Address
    
    transferAction :: Web3 HexString
    transferAction = MockERC20.transfer txOptions {to : recipient, amount : amount}
  
  -- await for a `Transfer` event emitted from contract with address 
  -- `mockERC20Address` after running `transferAction`
  Tuple _ (MockERC20.Transfer tfr) <- assertWeb3 provider $ 
    takeEvent (Proxy :: Proxy MockERC20.Transfer) mockERC20Address transferAction
    
  -- check that the transfer amount is the amount sent.
  tfr.amount `shouldEqual` amount
  ...

Continued from Previous Slide

  • Which parts are pure? Which are effectful?
     
  • Which parts could be throwing an exception?
     
  • Which parts are throwing null pointer exceptions (hint: none)
     
  • In the event that you need to refactor, what do you need to preserve?

What do you gain?

  • code : program :: types : metaprogram
     
  • The more accurate and expressive our types are, the more work the compiler will do for us (for free) to guarantee the program does what we want.
     
  • Downside: this kind of metaprogramming is hard. NOTE: Different than hard to get right.

Interesting Features

  • The parsers and ABI types are bullet proof, concise, and easily proved correct. [1]
     
  • Event Processing: coroutines, multifilter (ordering). [2]
     
  • Advanced FFI generation. [3]
     
  • Bullet proof error handling.
     
  • Automatic concurrency [4]

Ethereum Logs

  • mechanism to stream updates to contract state
  • Consumed via web3 filters

Multifilters problem statement

  1. You want to listen to multiple events, each coming from one of several contracts.
     
  2. You want to define specific handlers to run against each event type.
     
  3. You want to run each handler over it's event in the order that events were logged by the EVM (chronological order).

Multifilters Use Case

  1. You're building a cache for contract state which is updated / revalidated when certain events fire. (we call this an indexer).
     
  2. There are dependencies in your events. E.g. an NFT market contract has a TokenListedForSale event which refers to a token_id field.
     
  3. These dependencies create foreign key constraints in a relational database.
     
  4. You want to avoid running against a full archive node 

Future Work

Haskell Cosmos SDK

Martin Allen, Charles Crain, Irakli Safareli 

Starting Point

  • 3rd generation blockchain engineering is done in either golang or rust (some exceptions).
     
  • Tendermint is a replication engine that's agnostic to the language of the state machine.
     
  • Currently there is only one real implementation, limiting functional programmers involvement.
     
  • Haskell is an ideal language to write blockchain applications in.

hs-abci

  • FOAM awarded an interchain grant to complete a MVP for a Tendermint application SDK in Haskell.
     
  • Find the repo here.
     
  • Completed the bindings to the ABCI socket protocol, easy to start a server, hello world application.
     
  • Much work left to do ...

hs-abci

  • Focusing on creating a system of modules that compose, are easy to reason about, whose plumbing is handled by the compiler.
     
  • Design borrowed heavily from purescript-halogen.
     
  • Talk to me if you're interesting in status updates or getting involved.

Thank you

Don't miss our workshop

Day 2, Wednesday, 11.30am - 2.30pm A2