{"id":13681567,"url":"https://github.com/willtim/Expresso","last_synced_at":"2025-04-30T03:31:49.196Z","repository":{"id":4303558,"uuid":"5435530","full_name":"willtim/Expresso","owner":"willtim","description":"A simple expressions language with polymorphic extensible row types.","archived":false,"fork":false,"pushed_at":"2023-04-21T09:07:01.000Z","size":158,"stargazers_count":302,"open_issues_count":5,"forks_count":13,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-04-07T08:29:32.582Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/willtim.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2012-08-16T06:37:20.000Z","updated_at":"2025-03-25T15:28:31.000Z","dependencies_parsed_at":"2022-08-29T15:40:49.949Z","dependency_job_id":"b5030976-54f4-4408-bfd6-24db763ff772","html_url":"https://github.com/willtim/Expresso","commit_stats":{"total_commits":68,"total_committers":7,"mean_commits":9.714285714285714,"dds":"0.19117647058823528","last_synced_commit":"add7480c867b98819a482adb6bcd2da730928f1a"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willtim%2FExpresso","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willtim%2FExpresso/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willtim%2FExpresso/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willtim%2FExpresso/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/willtim","download_url":"https://codeload.github.com/willtim/Expresso/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251635392,"owners_count":21619213,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T13:01:32.370Z","updated_at":"2025-04-30T03:31:48.926Z","avatar_url":"https://github.com/willtim.png","language":"Haskell","readme":"# ☕ Expresso\n\nA simple expressions language with polymorphic extensible row types.\n\n\n## Introduction\n\nExpresso is a minimal statically-typed functional programming language, designed with embedding and/or extensibility in mind.\nPossible use cases for such a minimal language include configuration (à la Nix), data exchange (à la JSON) or even a starting point for a custom external DSL.\n\nExpresso has the following features:\n\n- A small and simple implementation\n- Statically typed with type inference\n- Structural typing with extensible records and variants\n- Lazy evaluation\n- Convenient use from Haskell (a type class for marshalling values)\n- Whitespace insensitive syntax\n- Type annotations to support first-class modules and schema validation use cases\n- Built-in support for ints, double, bools, chars and lists\n- Support for fixed-points (useful for dynamic binding), but without recursive records.\n\n## Installation\n\nExpresso the library and executable (the REPL) is currently built and tested using cabal.\n\n## Functions\n\nExpresso is a functional language and so we use lambda terms as our basic means of abstraction. To create a named function, we simply bind a lambda using let. I toyed with the idea of using Nix-style lambda syntax, e.g. `x: x` for the identity function, but many mainstream languages, not just Haskell, use an arrow to denote a lambda term. An arrow is also consistent with the notation we use for types.\nExpresso therefore uses the arrow `-\u003e` to denote lambdas, with the parameters to bind on the left and the expression body on the right, for example `x -\u003e x` for identity.\n\nNote that multiple juxtaposed arguments is sugar for currying. For example:\n\n    f x -\u003e f x\n\nis the same as:\n\n    f -\u003e x -\u003e f x\n\nThe function composition operators are `\u003e\u003e` and `\u003c\u003c` for forwards and backwards composition respectively.\n\n\n## Records\n\nExpresso records are built upon row-types with row extension as the fundamental primitive. This gives a very simple and easy-to-use type system when compared to more advanced systems built upon concatenation as a primitive. However, even in this simple system, concatenation can be encoded quite easily using difference records.\n\nRecords can of course contain arbitrary types and be arbitrarily nested. They can also be compared for equality. The dot operator (select) is used to project out values.\n\n    Expresso REPL\n    Type :help or :h for a list of commands\n    Loaded Prelude from /home/tim/Expresso/Prelude.x\n    λ\u003e {x = 1}.x\n    1\n    λ\u003e {x = {y = \"foo\"}, z = [1,2,3]}.x.y\n    \"foo\"\n    λ\u003e {x = 1, y = True} == {y = True, x = 1}\n    True\n\nNote that records cannot refer to themselves, as Expresso does not support type-level recursion.\n\n### Record extension\n\nRecords are eliminated using selection `.` and introduced using extension `|`. For example, the record literal:\n\n    {x = 1, y = True}\n\nis really sugar for:\n\n    {x = 1 | { y = True | {}}}\n\nThe row types use lacks constraints to prohibit overlapping field names. For example, the following is ill-typed:\n\n    {x = 1, x = 2} -- DOES NOT TYPE CHECK!\n\n    let r = {x = \"foo\"} in {x = \"bar\" | r} -- DOES NOT TYPE CHECK!\n\nThe lacks constraints are shown when printing out inferred row types via the REPL, for example:\n\n    λ\u003e :type r -\u003e {x = 1 | r}\n    forall r. (r\\x) =\u003e {r} -\u003e {x : Int | r}\n\nIn the above output, the REPL reports that this lambda can take a record with underlying row-type `r`, providing `r` satisfies the constraint that it does not have a field `x`.\n\nThe type of a literal record is *closed*, in that the set of fields is fully known:\n\n    λ\u003e :type {x = 1}\n    {x : Int}\n\nHowever, we permit records with redundant fields as arguments to functions, by inferring *open* record types:\n\n    λ\u003e let sqmag = {x, y} -\u003e x*x + y*y\n    λ\u003e :type sqmag\n    forall a r. (Num a, r\\x\\y) =\u003e {x : a, y : a | r} -\u003e a\n\nAn open record type is indicated by a row-type in the tail of the record.\n\nNote that the function definition for `sqmag` above makes use of field punning. We could have alternatively written:\n\n    λ\u003e let sqmag = r -\u003e r.x*r.x + r.y*r.y\n\nWhen matching on record arguments, sometimes it can be necessary to supply a new name to bind the values of a field to, for example:\n\n    λ\u003e let add = {x=r, y=s} {x=u, y=v} -\u003e {x = r + u, y = s + v}\n\n### Record restriction\n\nWe can remove a field by using the restriction primitive `\\`. For example, the following will type-check:\n\n    {x = 1 | {x = 2}\\x}\n\nWe can also use the following syntactic sugar, for such an override:\n\n    {x := 1 | {x = 1}}\n\n### First-class modules\n\nRecords can be used as a simple but powerful module system. For example, imagine a module `\"List.x\"` with derived operations on lists:\n\n    let\n        reverse     = foldl (xs x -\u003e x :: xs) [];\n        intercalate = xs xss -\u003e concat (intersperse xs xss);\n        ...\n\n    -- Exports\n    in { reverse\n       , intercalate\n       , ...\n       }\n\nSuch a module can be imported using a `let` declaration:\n\n    λ\u003e let list = import \"List.x\"\n    λ\u003e :type list.intercalate\n    forall a. [a] -\u003e [[a]] -\u003e [a]\n\nOr simply:\n\n    λ\u003e let {..} = import \"List.x\"\n\nRecords with polymorphic functions can be passed as lambda arguments and remain polymorphic using *higher-rank polymorphism*. To accomplish this, we must provide Expresso with a suitable type annotation of the argument. For example:\n\n    let f = (m : forall a. { reverse : [a] -\u003e [a] |_}) -\u003e\n                {l = m.reverse [True, False], r = m.reverse [1,2,3] }\n\nThe function `f` above takes a \"module\" `m` containing a polymorphic function `reverse`. We annotate `m` with a type by using a single colon `:` followed by the type we are expecting.\nNote the underscore `_` in the tail of the record. This is a *type wildcard*, meaning we have specified a *partial type signature*. This type wildcard allows us to pass an arbitrary module containing a `reverse` function with this signature. To see the full type signature of `f`, we can use the Expresso REPL:\n\n    λ\u003e :t f\n    forall r. (r\\reverse) =\u003e (forall a. {reverse : [a] -\u003e [a] | r}) -\u003e\n        {l : [Bool], r : [Int]}\n\nNote that the `r`, representing the rest of the module fields, is a top-level quantifier. The type wildcard is especially useful here, as it allows us to avoid creating a top-level signature for the entire function and explicitly naming this row variable. More generally, type wildcards allow us to leave parts of a type signature unspecified.\n\nFunction `f` can now of course be applied to any module satisfying the type signature:\n\n    λ\u003e f (import \"List.x\")\n    {l = [False, True], r = [3,2,1]}\n\n\n### Difference records and concatenation\n\nTo encode concatenation, we can use functions that extend records and compose them using straightforward function composition:\n\n    let f = (r -\u003e { x = \"foo\", y = True | r}) \u003e\u003e (r -\u003e { z = \"bar\" | r})\n\nExpresso has a special syntax for such \"difference records\":\n\n    λ\u003e let f = {| x = \"foo\", y = True |} \u003e\u003e {| z = \"bar\" |}\n    λ\u003e f {}\n    {z = \"bar\", x = \"foo\", y = True}\n\nConcatenation is asymmetric whenever we use overrides, for example:\n\n     {| x = \"foo\" |} \u003e\u003e {| x := \"bar\" |} -- Type checks\n     {| x = \"foo\" |} \u003c\u003c {| x := \"bar\" |} -- DOES NOT TYPE CHECK!\n\n### The Unit type\n\nThe type `{}` is an example of a *Unit* type. It has only one inhabitant, the empty record `{}`:\n\n    λ\u003e :type {}\n    {}\n\n\n## Variants\n\nThe dual of records are variants, which are also polymorphic and extensible since they use the same underlying row-types.\nVariants are introduced via injection (the dual of record selection), for example:\n\n    λ\u003e Foo 1\n    Foo 1\n\nUnlike literal records, literal variants are *open*.\n\n    λ\u003e :type Foo 1\n    forall r. (r\\Foo) =\u003e \u003cFoo : Int | r\u003e\n\nVariants are eliminated using the case construct, for example:\n\n    λ\u003e case Foo 1 of { Foo x -\u003e x, Bar{x,y} -\u003e x+y }\n    1\n\nThe above case expression eliminates a *closed* variant, meaning any value other than `Foo` or `Bar` with their expected payloads would lead to a type error. To eliminate an *open* variant, we use a syntax analogous to extension:\n\n    λ\u003e let f = x -\u003e case x of { Foo x -\u003e x, Bar{x,y} -\u003e x+y | otherwise -\u003e 42 }\n    λ\u003e f (Baz{})\n    42\n\nHere the unmatched variant is passed to a lambda (with `otherwise` as the parameter). The expression after the bar `|` typically either ignores the variant or delegates it to another function.\n\n### Closed variants\n\nWe will often need to create closed variant types. For example, we may want to create a structural type analogous to Haskell's `Maybe a`, having only two constructors: `Nothing` and `Just`. This can be accomplished using smart constructors with type annotations. In the Prelude, we define the equivalent constructors `just` and `nothing`, as well as a fold `maybe` over this closed set:\n\n    type Maybe a = \u003cJust : a, Nothing : {}\u003e;\n\n    just        : forall a. a -\u003e Maybe a\n                = x -\u003e Just x;\n\n    nothing     : forall a. Maybe a\n                = Nothing{};\n\n    maybe       = b f m -\u003e case m of { Just a -\u003e f a, Nothing{} -\u003e b }\n\nNote that we declare and use a type synonym `Maybe a` to avoid repeating the type `\u003cJust : a, Nothing : {}\u003e`. Type synonyms can be included at the top of any file and have global scope.\n\n### Variant embedding\n\nThe dual of record restriction is variant embedding. This allows us to restrict the behaviour exposed by a case expression, by exploiting the non-overlapping field constraints.\nFor example, to prevent use of the `Bar` alternative of function `f` above, we can define a new function `g` as follows:\n\n    λ\u003e let g = x -\u003e f (\u003c|Bar|\u003e x)\n    λ\u003e :type g\n    forall r. (r\\Bar\\Foo) =\u003e \u003cFoo : Int | r\u003e -\u003e Int\n\nEmbedding is used internally to implement overriding alternatives, for example:\n\n    λ\u003e let g = x -\u003e case x of { override Foo x -\u003e x + 1 | f }\n\nis sugar for:\n\n    λ\u003e let g = x -\u003e case x of { Foo x -\u003e x + 1 | \u003c|Foo|\u003e \u003e\u003e f }\n\n    λ\u003e :type g\n    forall r1 r2. (r1\\x\\y, r2\\Bar\\Foo) =\u003e \u003cFoo : Int, Bar : {x : Int, y : Int | r1} | r2\u003e -\u003e Int\n\n### The Void type\n\nInternally, the syntax to eliminate a closed variant uses the empty variant type `\u003c\u003e`, also known as *Void*. The Void type has no inhabitants, but we can use it to define a function `absurd`:\n\n    λ\u003e :type absurd\n    forall a. \u003c\u003e -\u003e a\n\nAbsurd is an example of *Ex Falso Quodlibet* from classical logic (anything can be proven using a contradiction as a premise).\n\nAs an example of the above, the following closed case expression:\n\n    case x of { Foo{} -\u003e 1, Bar{} -\u003e 2 }\n\nis actually sugar for:\n\n    case x of { Foo{} -\u003e 1 | x' -\u003e case x' of { Bar{} -\u003e 2 | absurd } }\n\n\n## A data-exchange format with schemas\n\nWe could use Expresso as a lightweight data-exchange format (i.e. JSON with types). But how might we validate terms against a schema?\n\nA simple type annotation `\u003cterm\u003e : \u003ctype\u003e` , will not suffice for \"schema validation\". For example, consider this attempt at validating an integer against a schema that permits everything:\n\n    1 : forall a. a        -- DOES NOT TYPE CHECK!\n\nThe above fails to type check since the left-hand-side is inferred as the most general type (here a concrete int) and the right-hand-side must be less so.\n\nInstead we need something like this:\n\n    (id : forall a. a -\u003e a) 1\n\nA nice syntactic sugar for this is a *signature section*, although the version in Expresso is slightly different from the Haskell proposal. We write `(:T)` to mean `id : T -\u003e T`, where any quantifiers are kept at the top-level. We can now use:\n\n    (: forall a. a) 1\n\nIf we really do have places in our schema where we want to permit arbitrary data, we should use the equality constraint to guarantee the absence of partially-applied functions. For example:\n\n    (: forall a. Eq a =\u003e { x : \u003cFoo : Int, Bar : a\u003e }) { x = Bar id }\n\nwould fail to type check. But the following succeeds:\n\n    λ\u003e (: forall a. Eq a =\u003e { x : \u003cFoo : Int, Bar : a\u003e }) { x = Bar \"abc\" }\n    {x = Bar \"abc\"}\n\n\n## Lazy evaluation\n\nExpresso uses lazy evaluation in the hope that it might lead to efficiency gains when working with large nested records.\n\n    λ\u003e :peek {x = \"foo\"}\n    {x = \u003cThunk\u003e}\n\n\n## Turing equivalence?\n\nTuring equivalence is introduced via a single `fix` primitive, which can be easily removed or disabled.\n`fix` can be useful to achieve open recursive records and dynamic binding (à la Nix).\n\n    λ\u003e let r = mkOverridable (self -\u003e {x = \"foo\", y = self.x \u003c\u003e \"bar\"})\n    λ\u003e r\n    {override_ = \u003cLambda\u003e, x = \"foo\", y = \"foobar\"}\n\n    λ\u003e override r {| x := \"baz\" |}\n    {override_ = \u003cLambda\u003e, x = \"baz\", y = \"bazbar\"}\n\nNote that removing `fix` and Turing equivalence does not guarantee termination in practice. It is still possible to write exponential programs that will not terminate during the lifetime of the universe without recursion or fix.\n\n## A configuration file format\n\nExpresso can be used as a typed configuration file format from within Haskell programs. As an example, let's consider a hypothetical small config file for a backup program:\n\n    let awsTemplate =\n        { location =\"s3://s3-eu-west-1.amazonaws.com/tim-backup\"\n        , include  = []\n        , exclude  = []\n        }\n    in\n    { cachePath   = Default{}\n    , taskThreads = Override 2\n    , profiles =\n       [ { name = \"pictures\"\n         , source = \"~/Pictures\"\n         | awsTemplate\n         }\n       , { name = \"music\"\n         , source = \"~/Music\"\n         , exclude := [\"**/*.m4a\"]\n         | awsTemplate }\n       ]\n    }\n\nNote that even for such a small example, we can already leverage some of the abstraction power of extensible records to avoid repetition in the config file.\n\nIn order to consume this file from a Haskell program, we can define some corresponding nominal data types:\n\n    data Config = Config\n        { configCachePath   :: Overridable Text\n        , configTaskThreads :: Overridable Integer\n        , configProfiles    :: [Profile]\n        } deriving Show\n\n    data Overridable a = Default | Override a deriving Show\n\n    data Profile = Profile\n        { profileName     :: Text\n        , profileLocation :: Text\n        , profileInclude  :: [Text]\n        , profileExclude  :: [Text]\n        , profileSource   :: Text\n        } deriving Show\n\nUsing the Expresso API, we can write `HasValue` instances to handle the projection into and injection from, Haskell values:\n\n    import Expresso\n\n    instance HasValue Config where\n        proj v = Config\n            \u003c$\u003e v .: \"cachePath\"\n            \u003c*\u003e v .: \"taskThreads\"\n            \u003c*\u003e v .: \"profiles\"\n        inj Config{..} = mkRecord\n            [ \"cachePath\"      .= inj configCachePath\n            , \"taskThreads\"    .= inj configTaskThreads\n            , \"profiles\"       .= inj configProfiles\n            ]\n\n    instance HasValue a =\u003e HasValue (Overridable a) where\n        proj = choice [(\"Override\", fmap Override . proj)\n                      ,(\"Default\",  const $ pure Default)\n                      ]\n        inj (Override x) = mkVariant \"Override\" (inj x)\n        inj Default = mkVariant \"Default\" unit\n\n    instance HasValue Profile where\n        proj v = Profile\n            \u003c$\u003e v .: \"name\"\n            \u003c*\u003e v .: \"location\"\n            \u003c*\u003e v .: \"include\"\n            \u003c*\u003e v .: \"exclude\"\n            \u003c*\u003e v .: \"source\"\n        inj Profile{..} = mkRecord\n            [ \"name\"     .= inj profileName\n            , \"location\" .= inj profileLocation\n            , \"include\"  .= inj profileInclude\n            , \"exclude\"  .= inj profileExclude\n            , \"source\"   .= inj profileSource\n            ]\n\nBefore we load the config file, we will probably want to check the inferred types against an agreed signature (a.k.a. schema validation). The Expresso API provides a Template Haskell quasi-quoter to make this convenient from within Haskell:\n\n    import Expresso.TH.QQ\n\n    schema :: Type\n    schema =\n      [expressoType|\n        { cachePath   : \u003cDefault : {}, Override : Text\u003e\n        , taskThreads : \u003cDefault : {}, Override : Int\u003e\n        , profiles :\n            [ { name     : Text\n              , location : Text\n              , include  : [Text]\n              , exclude  : [Text]\n              , source   : Text\n              }\n            ]\n        }|]\n\nWe can thus load, validate and evaluate the above config file using the following code:\n\n    loadConfig :: FilePath -\u003e IO (Either String Config)\n    loadConfig = evalFile (Just schema)\n\nNote that we can also install our own custom values/functions for users to reference in their config files. For example:\n\n    loadConfig :: FilePath -\u003e IO (Either String Config)\n    loadConfig = evalFile' envs (Just schema)\n      where\n        envs = installBinding \"system\" TText (inj System.Info.os)\n             . installBinding \"takeFileName\"  (TFun TText TText) (inj takeFileName)\n             . installBinding \"takeDirectory\" (TFun TText TText) (inj takeDirectory)\n             . installBinding \"doesPathExist\" (TFun TText TBool) (inj doesPathExist) -- NB: This does IO reads\n             $ initEnvironments\n\nFinally, we need not limit ourselves to config files that specify record values. We can project Expresso function values into Haskell functions (in IO), allowing higher-order config files! The projection itself is handled by the `HasValue` class, just like any other value:\n\n     Haskell\u003e Right (f :: Integer -\u003e IO Integer) \u003c- evalString (Just $ TFun TInt TInt) \"x -\u003e x + 1\"\n     Haskell\u003e f 1\n     2\n\n## References\n\nExpresso is built upon many ideas described in the following publications:\n* \"Practical type inference for arbitrary-rank types\" Peyton-Jones et al. 2011.\n* \"A Polymorphic Type System for Extensible Records and Variants\" B. R. Gaster and M. P. Jones, 1996.\n* \"Extensible records with scoped labels\" D. Leijen, 2005.\n","funding_links":[],"categories":["Haskell","Functional"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwilltim%2FExpresso","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwilltim%2FExpresso","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwilltim%2FExpresso/lists"}