Introduction to Functional Programming
Functional programming is a paradigm distinct from traditional object-oriented programming. It represents the future direction of programming language design, with newly developed languages increasingly incorporating functional features.
The author believes that the "next-generation computer science framework" will be built upon functional programming languages. Thus, mastering functional programming fundamentals is essential for programmers with a long-term perspective.
About This Series
This series shares the author's insights and practices on functional programming and blockchain through hands-on code analysis and classic literature interpretation. Currently, it focuses on two functional languages: Rust and Elixir, occasionally referencing others for supplementary context.
Introducing TaiShang
TaiShang is a recent functional + blockchain project by the author's team.
The TaiShang Alchemy Furnace enhances NFTs by adding "proof contracts" without modifying original NFT contracts, enabling features like combination, decomposition, lifecycle management, and rights binding—unlocking limitless innovation possibilities.
Vision 0x01: Empower all NFT-related projects with boundless commercial potential and gameplay.
Vision 0x02: Become next-generation blockchain infrastructure.
TaiShang serves as the first case study for exploring functional programming in this series.
This second installment demonstrates Ethereum data reading via Elixir-Ethereum interactions. Note: While Ethereum-focused, this process applies to all EVM-compatible blockchains (e.g., FISCO BCOS, Moobeam).
Ethereumex and ExABI
Two key Elixir repositories are used:
- Ethereumex:
https://github.com/mana-ethereum/ethereumex
An Elixir JSON-RPC client for Ethereum. ExABI:
https://github.com/poanetwork/ex_abi
Implements Solidity's Application Binary Interface (ABI), which defines how binary data maps to Solidity types.Tip:
ABI is the standard for interacting with EVM contracts..abifiles describe functions/events in JSON. Example:[{ "constant": true, "inputs": [], "name": "get", "outputs": [{"type": "string"}], "payable": false, "stateMutability": "view", "type": "function" }]
Setup
Add Ethereumex to mix.exs:
# mix.exs
def application do
[extra_applications: [:logger, :ethereumex]]
end
defp deps do
[{:ethereumex, "~> 0.7.0"}]
endConfigure node URL in config.exs:
config :ethereumex, url: "http://localhost:8545"Transaction Structure in Elixir
Ethereum transactions can be represented as Elixir structs:
%Transaction{
nonce: nonce, # Transaction counter (optional for read-only)
gas_price: @gas.price, # Fixed gas price
gas_limit: @gas.limit, # Gas cap
to: bin_to, # Binary address
value: 0, # ETH to send (0 for reads)
data: data # Encoded function call
}Note: nonce changes only for write operations.
eth_call Overview
The eth_call RPC method executes read-only calls without on-chain transactions:
{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{
"to": "0xCONTRACT_ADDRESS",
"data": "0xENCODED_FUNCTION"
}, "latest"],
"id": 1
}(Source: Ethereum Wiki)
Address Conversion
Convert standard addresses (e.g., 0x7696...601f) to binary:
@spec addr_to_bin(String.t()) :: binary()
def addr_to_bin(addr_str) do
addr_str
|> String.replace("0x", "")
|> Base.decode16!(case: :mixed)
endGenerating Call Data
Encode function calls using ABI:
@spec get_data(String.t(), list()) :: String.t()
def get_data(func_str, params) do
"0x" <> (
func_str
|> ABI.encode(params)
|> Base.encode16(case: :lower)
)
endFunction strings follow functionName(type1,type2) format, e.g., "balanceOf(address)".
Decoding Responses
Convert hex responses to usable formats:
defmodule TypeTranslator do
def data_to_int(raw) do
raw
|> hex_to_bin()
|> ABI.TypeDecoder.decode_raw([{:uint, 256}])
|> List.first()
end
def data_to_str(raw), do: ... # Similar for strings/addresses
endFull Elixir Contract Call Example
Fetching token balances:
@spec balance_of(String.t(), String.t()) :: integer()
def balance_of(contract_addr, addr_str) do
{:ok, addr_bytes} = TypeTranslator.hex_to_bytes(addr_str)
data = get_data("balanceOf(address)", [addr_bytes])
{:ok, balance_hex} = Ethereumex.HttpClient.eth_call(%{
to: contract_addr,
data: data
})
TypeTranslator.data_to_int(balance_hex)
endRust Equivalent (web3.rs)
use web3::{contract::Contract, types::H160};
#[tokio::main]
async fn main() -> web3::Result<()> {
let http = web3::transports::Http::new("https://ropsten.infura.io/v3/YOUR_KEY")?;
let web3 = web3::Web3::new(http);
let contract = Contract::from_json(
web3.eth(),
H160::from_slice(&hex!("7Ad11de6d4C3DA366BC929377EE2CaFEcC412A10")),
include_bytes!("contract.abi.json")
)?;
let result: String = contract.query("get", (), None, Options::default(), None).await?;
println!("{}", result);
Ok(())
}(Full example: eth-interactor-rs)
FAQ
1. Why use functional programming for blockchain interactions?
Functional programming emphasizes immutability and pure functions—qualities that align perfectly with blockchain's deterministic execution requirements.
2. How does ABI encoding work?
ABI encoding converts human-readable function calls into EVM-understandable binary data by:
- Hashing the function signature to get the 4-byte selector
- Packing arguments according to their types
3. What's the gas cost for eth_call?
eth_call consumes zero gas as it executes locally without mining. However, some nodes may impose rate limits.
4. Can I use these methods with Binance Smart Chain?
Absolutely! Any EVM-compatible chain (BSC, Polygon, etc.) uses identical interaction patterns—just change the RPC endpoint.
5. How do I handle dynamic return types?
Use pattern matching in Elixir or enums in Rust to process different ABI output types (e.g., (uint256,string)).