zaphar / ucg

A Universal Configuration Grammar
Apache License 2.0
36 stars 3 forks source link

Structural typing #55

Open zaphar opened 4 years ago

zaphar commented 4 years ago

Just playing around with some syntax here. I'm not completely sold on any of it yet but I need to get the first ideas out of my system.

Structural Typing with Shapes

UCG types are named Shapes for a ucg value. The defined shape is also the zero value for that Shape.

shape Width 0.0;
shape Height 0.0;

Shapes can be used as values. If assigned to a binding in a let statement they will be copied and the named binding will have that Shape as it's type.

let x = Width;

Any expression can be projected into a shape. If the Shape is a structural subset of the result of the expression then the projection succeeds. If the shape is not a structural subset then the projection fails and a ShapeError will be thrown.

0.0 :: Width;

The individual components of Compound Shapes can have an optional shape specifier. If no shape is associated with a component part then the default shape for the value is the shape for that component.

shape Box = {
    width :: Width = 0.0,
    height = 0.0,
};

List shapes specify all the allowed types in the list.

shape float_list = [float];

shape number_list = [float, int];

Let statements can have an optional shape specifier. If the RHS expression can't be projected into the shape defined by the type specifier then the compiler will throw a ShapeError. If the RHS can be projected into the Shape then the value will have the named shape as their Shape.

let w :: Width =  0.0;
let h :: Height = 0.0;

Function arguments can have an optional type specifier. Type specifiers check that the arguments passed in are the correct Shape. If the argument is not the same Shape then the compiler will throw a Shape Error.

let make_box = func(w :: Width, h :: Height) :: Box => {width :: w, height :: Height};

Module arguments can use Shapes as their default values. If no argument is passed in then the zero value for the shape is used. If values passed in are not of the same Shape then a ShapeError will be thrown.

let mymod = module{w=Width, h=Height} => (result :: Box)  {
   let result = make_box(mod.w, mod.h);
};

The default shape for a value is ANY; All values fit into the ANY Shape.

The primitive values all have predefined shapes. int float str list tuple.

Row Based Polymorphism with Anonymous Shapes

Using anonymous shapes allows us to accept any value that matches the same shape.

let make_url = func(u: {proto :: "http", domain :: "", path :: "/"}) :: str => 
    proto + "://" + domain + path :: str;

let url_mod = module{u={proto :: "http",
                                          domain="",
                                           path="/",
                                           fragment=""}} => (result: {to_str(): str})
{
  let make_url = func(u: {proto :: "http", domain :: "", path: "/"}) :: str => 
    proto + "://" + domain + path :: str;

  let result = {
    to_str = func() :: str => make_url(mod.u),
  };
};
frgomes commented 4 years ago

I feel the need of explicit strong types all over the place, but this may be just my bias towards strong type checking. Inferred types is definitely a nice feature but I would like to enforce explicit types wherever I see fit. For example:

Imagine that I have two variables holding Float values: weight and height. I would like to enforce the correct order when I pass these variables to a function. At the moment, both f(weight, height) and f(height, weight) compile fine whilst, ideally, the compiler should be warning us that we've passed arguments in the wrong order.

However, if I declare hypothetically something like:

type Weight = Float
type Height = Float

let weight: Weight = 72.5
let height: Height = 1.76

let f = func(height: Height, weight: Weight) { ... }

... the compiler would be able to bark warning me when I pass variables in the wrong order.

zaphar commented 4 years ago

I tend to like a mix. I like to infer it where I can but I also like to be explicit about the types as well. So I want to be able to infer when there is no explicit type constraint defined but I also want to be able to specify a constraint for the contract when it's useful.

zaphar commented 4 years ago

Just playing around with some syntax here. I'm not completely sold on any of it yet but I need to get the first ideas out of my system.

UCG types are named Shapes for a ucg value. The defined shape is also the 0 value for that Shape.

shape Width 0.0;
shape Height 0.0;

Any expression can be projected into a shape. If the Shape is a structural subset of the result of the expression then the projection succeeds. If the shape is not a structural subset then the projection fails and a ShapeError will be thrown.

0.0 :> Width;

Compound Shapes can have their components projected into previously defined Shapes.

shape Box = {
    width = 0.0 :> Width,
    height = 0.0 :> Height,
};

Let statements can have an optional type specifier. If the RHS expression can't be projected into the shape defined by the type specifier then the compiler will throw a ShapeError. If the RHS can be projected into the Shape then the value will have the named shape as their Shape.

let w: Width =  0.0;
let h: Height = 0.0;

Function arguments can have an optional type specifier. Type specifiers check that the arguments passed in are the correct Shape. If the argument is not the same Shape then the compiler will throw a Shape Error.

let make_box = func(w : Width, h : Height) => {width: w, height: Height}  :> Box;

Module arguments can use Shapes as their default values. If no argument is passed in then the zero value for the shape is used. If values passed in are not of the same Shape then a ShapeError will be thrown.

let mymod = module{w=Width, h=Height} => (result :> Box)  {
   let result = make_box(mod.w, mod.h);
};

The default shape for a value is ANY; All values fit into the ANY Shape.

The primitive values all have predefined shapes. int float str list tuple.

frgomes commented 4 years ago

I'm sorry for my long silence.

The defined shape is also the 0 value for that Shape.

Are you talking about monoids? Just curious. Please do not spend much effort on this question: it would be just distraction.

Regarding the trials with the syntax:

I'm confused since apparently there are different ways for expressing the same idea, which is: associate an explicit type to a variable.

One way is similar to several programming languages:

let w: Width =  0.0;

These below seem to be variations of another way:

shape Width 0.0;
shape Box = { ... }

And there's the general form value :> shape which appears on snippets like:

width = 0.0 :> Width;
{width: w, height: Height}  :> Box;
(result :> Box)

I'm puzzled whether or not there's a deep reason behind these variations. Maybe I'm just missing something.

You explained the role of ':>' which is similar, if not the same, as it is in Scala, and I feel familiar with it. having the bias to interpret ':>' as variance (as opposed to invariance or contravariance) but I'm not plenty sure if I understood your intentions properly. Besides, I'm not fully skilled on ucg.

My suggestion (or concern) is related to various slight different ways involved to express basically the same concept. If there's only one general form which could fit into various contexts, this would make the syntax easier to grasp and understand.

zaphar commented 4 years ago

the shape syntax defines the type instead of associating a type to a variable. The :> construction was a way of post-hoc asserting that a variable fits into a given shape. the result would be a new value with that shape as it's type.

This is just the first pass though to get the bad ideas out of my system :-D

zaphar commented 4 years ago

As to the first question no it wouldn't fit the definition of a monoid since there is no defined sequence there. It's more like the default value. It's useful for the module construction syntax.