Records and tuples
Records and tuples group several values into one. This page shows how to create, read, update and destructure them, and how Elci encodes them as on-chain data. That encoding matters as soon as a record becomes a datum or redeemer.
Records
Section titled “Records”A record has named fields. type alias gives a record type a name:
module Main exposing (main)
type alias Listing = { price : Int , active : Bool }
discount : Int -> Listing -> Listingdiscount percent listing = { listing | price = listing.price - listing.price * percent // 100 }
main : Intmain = let original = { price = 2000, active = True }
cheaper = discount 25 original in if cheaper.active then cheaper.price
else 01500cpu 5077108, mem 17933{ price = 2000, active = True }creates a record. It has typeListingbecause it has exactly those fields.listing.pricereads a field.{ listing | price = ... }is a record update: a copy oflistingwith new values for the named fields. You can update several fields at once, separated by commas. The record to update must be a plain name.
Records are values. An update creates a new record and leaves the original unchanged.
Record constructors
Section titled “Record constructors”A record alias also defines a constructor function. Its arguments follow the order of the fields in the declaration:
module Main exposing (main)
type alias Listing = { price : Int , active : Bool }
main : Listingmain = Listing 2000 True{ active = True, price = 2000 }cpu 886960, mem 4025Listing 2000 True is the record { price = 2000, active = True }. elci eval prints record fields in
alphabetical order, which is a hint about how Elci stores them.
Accessor functions
Section titled “Accessor functions”As in Elm, .amount is a function that reads the field amount of a record. It is short for
\payout -> payout.amount, and it is handy with list functions:
module Main exposing (main)
type alias Payout = { beneficiary : String , amount : Int }
main : ( Int, List String )main = let payouts = [ Payout "carol" 300, Payout "alice" 150, Payout "bob" 250 ] in ( List.sum (List.map .amount payouts) , payouts |> List.sortBy .amount |> List.map .beneficiary )( 700, ["alice", "bob", "carol"] )cpu 31097157, mem 139215A record that another module exposes is read the same way. Config.settings.fee reads the field fee
of the value settings in module Config:
module Config exposing (Settings, settings)
type alias Settings = { fee : Int , owner : String }
settings : Settingssettings = { fee = 200, owner = "alice" }module Main exposing (main)
import Config
main : Intmain = Config.settings.fee * 2400cpu 1310048, mem 4828A qualified name is written without spaces and ends at its first lowercase part: Config.settings is the
value, and .fee reads its field. In List.map .amount payouts, the space before .amount makes it an
accessor passed to List.map.
How records are stored on-chain
Section titled “How records are stored on-chain”Cardano stores datums and redeemers as Plutus Data: integers, byte strings, lists, maps and constructors
(a tag number with a list of fields). Julc.Data.encode turns any value into its Data, so you can see
exactly what goes on-chain:
module Main exposing (main)
import Julc.Data as Data
type alias Listing = { price : Int , active : Bool }
main : Datamain = Data.encode (Listing 2000 True)Constr 0 [Constr 1, 2000]cpu 982960, mem 4625- A record is a constructor with tag 0.
- Its fields are sorted alphabetically by name, whatever order you declared them in. That is why
active(True, itself encoded as constructor 1 with no fields) comes beforeprice(2000).
This has practical consequences for datums and redeemers:
- Reordering the fields of a record declaration doesn’t change its encoding.
- Renaming a field can change the encoding, because it can change the sort order. Data already on-chain
would no longer decode. The build’s
abi.jsonrecords the exact layout, so you can detect such changes.
Datums and redeemers covers the encoding rules for every type.
Destructuring
Section titled “Destructuring”Patterns can take a record apart. { amount } binds the field amount to a variable of the same name. It
works in function and lambda parameters, in let, and in case:
module Main exposing (main)
type alias Payout = { beneficiary : String , amount : Int }
total : List Payout -> Inttotal payouts = List.foldl (\{ amount } sum -> amount + sum) 0 payouts
main : Intmain = let payouts = [ Payout "alice" 150, Payout "bob" 250 ]
{ beneficiary } = Payout "carol" 10 in if beneficiary == "carol" then total payouts
else 0400cpu 6314459, mem 26311In a case, a record pattern selects fields and can sit inside other patterns, such as tuples:
module Main exposing (main)
type alias Order = { kind : String , quantity : Int , paid : Bool }
priority : Order -> Intpriority order = case order of { paid, quantity } -> if paid && quantity > 10 then 2
else if paid then 1
else 0
kindCode : Order -> IntkindCode order = case ( order.kind, order.paid ) of ( "bulk", True ) -> 3
( "bulk", False ) -> 2
( _, _ ) -> 1
main : Intmain = let order = { kind = "bulk", quantity = 25, paid = True } in priority order * 10 + kindCode order23cpu 7369610, mem 25543Generic records
Section titled “Generic records”A record alias can take type parameters, like a function takes arguments:
module Main exposing (main)
type alias Tagged a = { label : String , value : a }
relabel : String -> Tagged a -> Tagged arelabel label tagged = { tagged | label = label }
main : Intmain = let amount = relabel "fee" (Tagged "draft" 170000)
flag = Tagged "enabled" True in if flag.value && amount.label == "fee" then amount.value
else 0170000cpu 5150199, mem 18573amount is a Tagged Int and flag is a Tagged Bool. relabel works on both.
Tuples
Section titled “Tuples”A tuple groups two or three values without naming them: ( 1, "one" ) has type ( Int, String ). Tuples are
handy for returning several results from a function:
module Main exposing (main)
divMod : Int -> Int -> ( Int, Int )divMod amount parts = ( amount // parts, modBy parts amount )
main : Intmain = let ( quotient, remainder ) = divMod 17 5 in quotient * 10 + remainder32cpu 3397721, mem 12146The Tuple module, which every module imports, works on pairs: Tuple.first, Tuple.second,
Tuple.pair, Tuple.mapFirst, Tuple.mapSecond and Tuple.mapBoth. Tuples of comparable values, such as
numbers and strings, compare component by component, so List.sort sorts them:
module Main exposing (main)
main : ( List ( Int, String ), List Int )main = let scores = [ ( 2, "bob" ), ( 1, "carol" ), ( 2, "alice" ) ] in ( List.sort scores, List.map Tuple.first scores )( [( 1, "carol" ), ( 2, "alice" ), ( 2, "bob" )], [2, 1, 2] )cpu 28573982, mem 123050A tuple is encoded like a record, as a constructor with tag 0, but its fields stay in position order. For anything with more than three parts, or whose parts deserve names, use a record. A four-element tuple is rejected:
let ( a, b, c, d ) = ( 1, 2, 3, 4 ) in.../src/Main.elm:7:24: ELCI1002: Tuple patterns have two or three elements
7| ( a, b, c, d ) = ^Equality
Section titled “Equality”== compares records and tuples field by field. Field order in the literal doesn’t matter:
module Main exposing (main)
main : Boolmain = ( 1, "a", True ) == ( 1, "a", True ) && { x = 1, y = 2 } == { y = 2, x = 1 }Truecpu 6141566, mem 15472Records have no order, so < and List.sort reject them. Sort records by a field with
List.sortBy (\listing -> listing.price); see sorting.
Limits
Section titled “Limits”Records in Elci are closed: a record type lists all of its fields.
-
Extensible record types such as
{ r | price : Int }are not supported:.../src/Main.elm:4:13: ELCI1002: Extensible records are unsupported; use a closed record4| total : { r | price : Int } -> Int^ -
A field access, or an accessor function such as
.price, needs to know which record type it is reading. Use a named alias or a signature when inference can’t tell. Reading a field that doesn’t exist is an error:.../src/Main.elm:14:12: ELCI2010: Missing field 'cost'; available fields: [price]14| listing.cost^
Custom types and pattern matching models values that can take different shapes.