Projections and Field Access

Enso provides multiple ways for users to access data from their types. It has the old functional stalwart of pattern matching, but it also has an inbuilt notion of accessors based on lenses (field projections).

Pattern Matching

Pattern matching in Enso works similarly to as you would expect in various other functional languages. Typing information is always refined in the branches of a case expression, which interacts well with dependent typing and type-term unification. There are a few main ways you can pattern match:

  1. Positional Matching: Matching on the scrutinee by structure. This works both for atoms and typesets (for typesets it is a subsumption judgement).

    type Vector a
      V2 x:a y:a
      V3 x:a y:a z:a
    
    v = Vector.V3 x y z
    
    case v of
      Vector.V3 x y z -> print x
    
  2. Type Matching: Matching purely by the types involved, and not matching on structure.

    case v of
      Vector.V3 -> print v.x
    
  3. Name Matching on Labels: Matching on the labels defined within a type for both atoms and typesets, with renaming.

    case v of
      Vector.V3 {x y} -> print x
      {x}             -> print x
    
  4. Naming Scrutinees in Branches: Ascribing a name of a scrutinee is done using the standard typing judgement. This works due to the type-term unification present in Enso.

    case _ of
      v : Vector.V3 -> print v,x
    

The actionables for this section :

  • Refine the syntax for the name-based case.
  • Provide code examples for why the renaming use-case is important (e.g. cases where there are clashing field names).
  • Function-resolution matching.

The Underscore in Pattern Matching

An underscore _ passed as an argument to a syntactic pattern does not behave like the function argument shorthand. Instead it acts as a positional match that is given no name.

Projections

Unlike the simple accessors defined by most programming language, Enso’s accessors are far more powerful. This is because they are based on lenses.

  • Field accessors are standard lenses. As such, they can be used for both the getting and setting of properties.
  • As a lens (e.g. .field) is a first-class function, they can be curried and passed around like any other function.
type Engine
    type Combustion
        power:          Int
        cylinder_count: Int

    type Electric
        power:   Int
        is_blue: Bool

type Vehicle
    type Car
        color:     String
        max_speed: Int
        engine:    Engine

    type Bike
        color: String

type Person
    type Cons
        name:    String
        vehicle: Vehicle

main =
  p1 = Person.Cons "Joe" (Vehicle.Car 'pink' 300 (Engine.Combustion 500 8))
  print $ p1.name                   # -> Joe
  print $ p1.vehicle.color          # -> pink
  print $ p1.vehicle.max_speed      # -> Some 300
  print $ p1.vehicle.engine.power   # -> Some 500
  print $ p1.vehicle.engine.is_blue # -> None
  p1.vehicle.color     = 'red'      # OK
  p1.vehicle.max_speed = 310        # FAIL: security reasons. Allowing this
                                    #       in Haskell was the worst decision
                                    #       ever. After refactoring it
                                    #       silently does nothing there.

  p2 = p1.vehicle.max_speed    ?= 310 # OK
  p3 = p1.vehicle.engine.power  = 510 # FAIL
  p4 = p1.vehicle.engine.power ?= 510 # OK

  lens_name      = .name
  lens_color     = .vehicle.color
  lens_max_speed = .vehicle.max_speed
  lens_power     = .vehincle.engine.power

  ## Function like usage:
  print $ lens_name      p1
  print $ lens_color     p1
  print $ lens_max_speed p1
  print $ lens_power     p1

  p1 . at lens_name = ... # OK

The actionables for this section are:

  • Fix the example above. It isn’t correct.