Skip to content

The Language (Differences between vanilla PureScript and Purus)

gnumonik edited this page Jan 20, 2025 · 3 revisions

Preamble

While we have endeavored to make Purus as close to PureScript as possible, there are several places where the requirements of UPLC compilation necessitate a few divergences from vanilla PureScript. This page consists of a list of those changes, along with (hopefully) enough of an explanation to allow users familiar with PureScript to quickly adjust.

Row Type Syntax

Due to an ambiguity in PureScript's parser which prevents kind annotations of type variables in contexts where they may be necessary for successful compilation to UPLC, we have altered the syntax for Rows of types in PureScript.

In vanilla PureScript, the syntax for a row type is:

(field1 :: Type1, field2 :: Type2, field3 :: Type3)

In Purus, if you wish to write out a row type, you should use the syntax:

[field1 :: Type1, field2 :: Type2, field3 :: Type3]

IMPORTANT NOTE: This changes ONLY applied to Row types, and not to records. The type of a record in both vanilla PureScript and Purus may be represented with the normal syntax: {field1 :: Type1, field2 :: Type2, field3 :: Type3}. Since most users are likely to interact with Rows only through the records they support, this change should not be a concern for a majority of users.

Polymorphic Rows/Records Must Be Instantiated

Both vanilla PureScript and Purus support polymorphic rows/records, which are most commonly encountered in a form like:

gimmeAnA :: forall (r :: Row Type). {a :: Int | r} -> Int 
gimmeAnA r = r.a 

(The quantifier and type annotation are required in Purus, but not in PureScript, for reasons that will be explained below).

However, there is a significant difference between vanilla PureScript and Purus: Because Plutus does not and cannot support the Row kind, you must ensure that all function expressions containing a polymorphic record are applied to arguments with a concrete type.

Briefly, the reason for this is that records are translated into tuples during UPLC compilation. For example, the non-polymorphic record {foo :: Int, bar :: Bool} will be translated into a Tuple2 Int Bool. In order to support accessors for record fields (e.g. x.foo), it is necessary that the types of all fields are known at compile time - if they aren't known, we cannot determine which tuple index corresponds to the record labels (foo/bar here). This is unproblematic for records with a static and known type, such as the previous example. But this restriction poses a problem for polymorphic records appearing in function signatures, since the structure of the "rest of the row" variable (r in the example above) is not known until that function is applied.

In practice, users do not need to understand this restriction, and following the rule: MAKE SURE YOUR MAIN FUNCTION DOES NOT HAVE ANY POLYMORPHIC/OPEN RECORDS will be sufficient to satisfy the requirement.

No foreign imports

While we could, in theory, support some kind of FFI with either PIR, PLC, or UPLC, doing so does not seem very useful and increases compiler complexity quite a bit, so we have disabled the ability to declare foreign imports of every sort.

Quantifiers / Type Variable Defaulting

In Purus, unlike PureScript, explicit quantifiers are required for all type variables.

This is, this will compile fine:

aFunction :: forall x. x -> Int

Whereas this will throw an error and fail to compile:

aFunction' :: x -> Int

This change was not essential, but we chose to require explicit quantifiers in order to increase the likelihood of confusion or mistakes resulting from failure to properly annotate the kind of type variables.

Purus (unlike PureScript), performs type variable defaulting. Specifically, if Purus encounters a type variable without a kind annotation, it defaults the kind of that type variable to kind Type. This seems like a happy medium between requiring kind annotations everywhere (which can be a bit tedious) and modifying the vanilla kind inference machinery to suit our needs.

In practice, this change mean that you should be careful to annotate type variables with :: Row Type if you are using a polymorphic row in an open record. It is of course good practice to annotate all type variables with their kind.

Users who do not make use of polymorphic rows / open records can largely ignore this change - though you will still have to use explicit quantifiers.

(TEMPORARY) Multi-scrutinee Case Expressions Are Not Supported

Vanilla PureScript allows for multi-scrutinee case expressions, e.g.

multiCase :: Int -> Int -> Int 
multiCase x y = case x,y of 
  _,0 -> 0 
  a,b -> a `div` b

While Purus supports these "under the hood" (i.e. the PureScript compiler occasionally generates multi-scrutinee case expressions during desugaring, and these can be successfully compiled to UPLC), they give rise to some problems in the parser, and are temporarily disabled until we can sort out the parser issues.

Clone this wiki locally