Functional Programming for Ethereum
Topics
- Introduction
- The State of FP in Ethereum
- Simplify Your Life
- 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)
-
Mapcovery
-
Android + Gnosis Safe + FOAM wallet recovery
-
Android + Gnosis Safe + FOAM wallet recovery
-
Google + Chainlink integration
-
Google cloud + Kovan FOAM + Chainlink
-
Google cloud + Kovan FOAM + Chainlink
-
foam.tools third party apps
- Google cloud + FOAM API
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)
- 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
- Decision to write Spatial Index (Beta) in PureScript.
- Decision to write native web3 library from scratch.
- Release feature complete version of purescript-web3
- Truffle migraine 🤕 . Decision to write replacement.
- 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
- Codecs
- Throwing an exception / indicating an error
- Invoking a web3 or other network call
Examples of Pure Functions
- Data transformations that can't fail
- 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)
Excerpts from Network.Ethereum.Core.Signatures
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)
Taken from Web3Spec.Live.MockERC20Spec
...
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
Ethereum Logs
- mechanism to stream updates to contract state
- Consumed via web3 filters
Multifilters problem statement
- You want to listen to multiple events, each coming from one of several contracts.
- You want to define specific handlers to run against each event type.
- 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
- You're building a cache for contract state which is updated / revalidated when certain events fire. (we call this an indexer).
- There are dependencies in your events. E.g. an NFT market contract has a TokenListedForSale event which refers to a token_id field.
- These dependencies create foreign key constraints in a relational database.
- 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
Functional programming for Ethereum
By Kristoffer
Functional programming for Ethereum
Presentation at Devcon 5, Osaka, Japan
- 1,839