Skip to content

Instantly share code, notes, and snippets.

@br3nt
Last active March 28, 2024 04:12
Show Gist options
  • Save br3nt/8f610a3c92de2e30f41f579b10647e6d to your computer and use it in GitHub Desktop.
Save br3nt/8f610a3c92de2e30f41f579b10647e6d to your computer and use it in GitHub Desktop.
Ideas for a less verbose language as beautiful as Ruby

Inspiration

I really dislike repeating my self in code I really dislike writing code just to get around language limitations

Typed languages are the absolute worst for code verbosity. The need to create object upon object to wrap and transform and present data is crazy

However, I do appreciate the type checking that goes along with Typed languages. I feel dynamic languages could actually acheive this by having the compiler create virtual interfaces for objects passed to methods. If an object is passed to multiple methods, it must implement all of these virtual interfaces.

I feel ruby is on the right track from being a perfect language This document defines all the additional features I wish I had in a programming language

New Features in addition to Ruby

Classes

  • Auto-setting instance attributes from constructor initialization
  • Creating accessors, readers, and writers
  • Setting default values for instance attributes for readers
  • default values for getter/setter
  • multi-line reader/writer
  • single line method definitions
  • set attributes directly using positional and name params in method calls
  • syntactic sugar for empty variable
  • default method parameter values for nullable and empty variables (in addition to optional arguments)
  • multiple inheritance
  • decorator objects
  • refinements
  • interfaces and abrstract classes
  • generic classes
  • type conversion

Objects

  • nullable objects
  • boolean objects
  • ternary objects (can be == compared with nil, true, and false)

Methods

  • method overloading based on method signature
  • method shorthand syntax
  • optional end (the next call to def closes the previous method and starts the next) - whitespace and indentation still DO NOT matter
  • passing methods by name
  • extend a method instead of override it

Syntactic sugar

  • ||= operator assigns to unassigned or nil variables
  • ??= operator assigns to unassigned, nil, .empty? and .blank? variables
  • =!! an assignment operator that disallows assignment of unassigned and nil values
  • =?? an assignment operator that disallows assignment of empty/blank values

Code flow

  • Assigning a case value to a variable in a matching when block
  • Enumerator value piping with the -> or |> and -| operators

Compiling and duck-type checking

  • Objects passed to methods are checked for the presence of methods that will be called on them
  • Ability to define code generators that can be injected into the compiler. Eg. repetative code could instead be inserted into the AST and made available to the running application An example of this might be creating serialize/deserialize JSON methods that work on the properties of a class

Simple example in Rust of why variance is needed:

Here's a simple example of this happening when we apply subtyping in a completely naive "find and replace" way.

fn evil_feeder(pet: &mut Animal) {
  let spike: Dog = ...;

  // `pet` is an Animal, and Dog is a subtype of Animal,
  // so this should be fine, right..?
  *pet = spike;
}

fn main() {
  let mut mr_snuggles: Cat = ...;
  evil_feeder(&mut mr_snuggles);  // Replaces mr_snuggles with a Dog
  mr_snuggles.meow();             // OH NO, MEOWING DOG!
}

Clearly, we need a more robust system than "find and replace". That system is variance, which is a set of rules governing how subtyping should compose. Most importantly, variance defines situations where subtyping should be disabled.

Firstly, I don't yet understand why a Dog value could be assigned to an Animal variable when the underlying type is a Cat.

Enums are good, but enums on their own are kind of useless and no better than using ints or strings. The true power of enums comes from being able to associate an enum value with specific values.

Java style enums are the bomb!. Have a look at the planet example. They have constructors and methods!

Rust enums are also cool. They can define different constructors for different enum values. I'm not sure how useful that is, or how you access the constuctor values safely. Methods can also be added to Rust enums!

Swift enums. Like the Rust enums, they can also define different constructors for different enum values. The values can be deconstructed using pattern matching in switch statements. Swift calls this associated values, and claims that the concept is like descriminated unions, however, I feel that it is a poor man's version of discriminated unions as they are too closely tied to the enum rather than types.

I think typed enums are the way to go. However, I would like the ability to use a separate class (or shape) to back the enum. This gives us type safety for each enum value. It also allows us to reuse classes we have already defined. It also allows multiple enums to be defined using the same type.

You could almost replace the word enum with list, but I think this would confuse devs coming from other languages.

class AuthenticationScope(token, user_class)

enum AuthenticationScopes < AuthenticationScope
  User = new(:user, ::User)
  Corporate = new(:corporate, CorporateUser)
  Admin = new(:admin, Administrator)
  Developer = new(:developer, ::Developer)
end

I don't think an enum should be backed by an int or value by default. By default, the enum value should be of type EnumValue. When to_s is called, the name of the enum value should be returned.

enum SomeEnum
  First
end

SomeEnum.First.to_s # "First"

The enum list should also have default methods like values, and enumeration methods like each, map, filter, etc. In addition, custom methods could be defined:

class Planet(mass, radius, rocky, gas_giant)

enum Planets < Planet
  Mercury = new(3.303e+23, 2.4397e6, true, false),
  Venus = new(4.869e+24, 6.0518e6, true, false),
  Earth = new(5.976e+24, 6.37814e6, true, false),
  Mars = new(6.421e+23, 3.3972e6, true, false),
  Jupiter = new(1.9e+27,   7.1492e7, false, true),
  Saturn = new(5.688e+26, 6.0268e7, false, true),
  Uranus = new(8.686e+25, 2.5559e7, false, true),
  Nepturne = new(1.024e+26, 2.4746e7, false, true);

  def rocky => values.filter(p => p.rocky?)
  def gas_giants => values.filter(p => p.gas_giant?)
end

var rocky_planets = Planets.rocky
var gas_giants = Planets.gas_giants

When using an enum type, effectively the enum type is a singleton instance.
The methods defined for the enum are acting on the instance of the enum type, not the enum values.
The concept is similar to the Eigenclass concept in ruby.

Enum flags

Do we need enum flags?

The bitwise operations that generally apply to enum flags could easily be made more generic by having them apply to any set or collection concept.
By doing this, we can have collections of enums, and use bitwise operations like |, &, etc to deterimine and manipulate properties of collections of the enum values without having to assign the enum values powers of 2.
It also means these operators can be used in more places.

var fun_planets = Planets.Venus | Planets.Mars # build a set of planets
var fun_planets |= Planets.Uranus # add more planets to the set
var is_a_fun_planet = fun_planets & Planets.Uranus # check if uranus is in the set

A drawback to this is that most devs probably wouldn't be expecting this behaviour.

It also conflates the usage of the bitwise operators.. though maybe thats ok to have the operators work differently n different places?

An alternative would e to have specific set operators that are different from the bitwise operators.

Enums and local or global keywords

Think how true and false are two possible values for a boolean. The true annd false are both literals and keywords.

Like boolean values, enums also represent discrete values. In order to use an enum value, both the enum type identifier and enum value identifier must e specified.

What if, we could assign a keyword to an enum?

This might be useful when particular enum values are predominant in a particular domain. Assigning a keyword to an enum value signifies the importance of it. It rules out the possibility that the keyword can be used for any other purpose.

What should the scope of the keyword be? We may wish to have the keyword global. We may wish to scope the keyword to a file, or a module.

Consideration should also be given when importing the code as a library. The keywords should not be exported by default, if at all.

Also, consideration should be given that a keyword could really be anything. We could specify the pi or tau keyword for example.

```ruby
class X
# creates getters and setters for the given attributes/properties
attributes a, b, c
att a, b, c
@ a, b, c
# `@` represents the internal variable backing the x property
get x -> @
# `value` represents the incoming value to be assigned to the internal `@` property backing the x property
set x -> @ = value
# provide a default value on on initialization
get x -> { @ } = 'default value'
# provide default values when attribute is empty
get x -> @ ?? 'default value'
# provide a default value on on initialization
get x -> { @ } = 'default value'
# provide default values on initialization and when `attribute` is empty
get x -> { @ ?? 'default value' } = 'default value on initialization'
# provide default values when `attribute` is empty
get x -> @ ?? 'default value'
# provide default values when `value` is empty
set x -> @ = value ?? 'default value'
end
```

No params

Multi-line method:

def hey
  'hello world!'
end

hey # 'hello world!'

Single-line method:

def hi 'hello world!'
def hi -> 'hello world!' # use optional arrow keyword for visual distinction

hi # 'hello world!'

Methods with params

Multi-line method:

def hey(thing)
  'hello #{thing}!'
end

hey('world') # hello world!

Single-line method:

def hi(thing) 'hello #{thing}!'

hi('world') # hello world!

Lambdas (aka anonymous functions)

Lambda/anonymous function with no params:

x = -> 'Hello world'
x = -> { 'Hello world' }

x = ->() 'Hello world'
x = ->() { 'hello world' }

x # Function
x.callable? # true
x() # 'Hello world'

Lambda/anonymous function with params:

x = ->(thing) 'Hello #thing'
x = ->(thing) { 'Hello #thing' }

x('world') # 'Hello world'


x = ->(greeting, thing) '#greeting #thing'
x = ->(greeting, thing) { '#greeting #thing' }
x = ->(greeting, thing) do
  '#greeting #thing'
end

x('hello', 'world') # 'hello world'
String interpolation with string:
```
object = 'world'
'hello #object' # hello world
```
String interpolation with number:
```
object = 123
'hello #object' # hello 123
```
String interpolation with method call (note the `#{}`):
```
object = [:world]
'hello #{object.first}' # hello world
```

Given the syntax:

var externalVariable
array.where(n => someCondition(n, externalVariable))

It would be nice to be able to rewrite it as:

var externalVariable
array.where(someCondition(&0, externalVariable))

The number after the & indicates the position of the argument.

array.where(someCondition(&0, &1, externalVariable))

For languages that support keyword arguments, the & could also be followed by the keyword.

array.where(someCondition(&arg2, &arg1, externalVariable))

It would be nice if we could simply use & to refer to the first argument:

var externalVariable
array.where(someCondition(&, externalVariable))

And with multiple arguments:

array.where(someCondition(&, &1, externalVariable))

What could this look like in languages that provide destructuring support?

Map destructuring

var externalVariable
array.where({ n } => someCondition(n, externalVariable))

could be rewritten as:

var externalVariable
array.where(someCondition(&0{ n }, externalVariable))

Or, with the first argument shortcut:

var externalVariable
array.where(someCondition(&{ n }, externalVariable))

Array destructuring

var externalVariable
array.where([a, b] => someCondition(a, b, externalVariable))

could be rewritten as:

var externalVariable
array.where(someCondition(&0[0], &0[1], externalVariable))

Or, with the first argument shortcut:

var externalVariable
array.where(someCondition(&[0], &[1], externalVariable))

This needs work

Wha doesn't work with the below examples:

  • Specifying a generic type, but expecting that type to be an instance, and then using it as an instance is werid.
  • Instantiating objects when defining a type is weird
  • Defining a generic type with null, and then the params of that type just dissapear is really weird.
    • Maybe if instead void is used that could work?

Generics wishlist

[ ] Ability to use objects in the generics parameter:

Using null

Using the following example generic class:

class Container<T> {
  T t
  T t2 { get; set; }
}

Has the following effect:

class MyContainer extends Container<null> {
  null t # t just references null
  null t2 { get; set; } # gets and sets null
}

Using the following example generic class:

class Request<Params, Response> {
  Response fetch(Params params)
}

Using null for the Params generic parameter has the effect of removing it from the paramater list of the method:

class MyRequest extends Request<null, MyResponse> {
  @override
  MyResponse fetch() => new Response
}

Using null for the Response generic parameter has the effect of returning null from the method:

class MyRequest extends Request<MyParams, null> {
  @override
  null fetch(MyParams params) => someFunction(params)
}

Using the following example generic class:

class List<T> {
  void Add(T t)
  T Remove(T t)
  T ElementAt(int i)
}

Using null has the following effect:

class List<null> {
  void Add()
  null Remove()
  null ElementAt(int i)
}

Using objects

Using the following example generic class:

class Request<PathProvider, Params, Response> {
  baseUrl => 'some_url'
  requestUrl => `$baseUrl\${PathProvider.requestPath}`
  Response fetch(Params params)
}

PathProvider must have method requestPath.

class MyPath extends PathProvider {
  requestPath => 'some_path'
}

class MyRequest extends Request<MyPath.new, MyParams, MyResponse> {
  ...
}

Using multiple types and shapes

class Request<PathProvider is String || { requestPath }, Params, Response> {
  baseUrl => 'some_url'

  request_path =>
    case PathProvider
    when String : PathProvider
    when { requestPath } : PathProvider.requestPath
    end

  requestUrl => `$baseUrl\${requestPath}`

  Response fetch(Params params)
}

Accepts object as per previous example:

class MyPath extends PathProvider {
  requestPath => 'some_path'
}

class MyRequest extends Request<MyPath.new, MyParams, MyResponse> {
  ...
}

Also accepts a string:

class MyRequest extends Request<'my_path', MyParams, MyResponse> {
  ...
}

class MyOtherRequest extends Request<'my_other_path', MyParams, MyResponse> {
  ...
}

To make the syntax a little easier, a shape can be defined:

shape PathProvider<String || { to_s } as to_s || { request_path } as request_path>
  def to_s
    case @
    when String       : @
    when to_s         : @.to_s
    when request_path : @.request_path
    end
  end
end

class Request<PathProvider request_path, Params, Response> {
  baseUrl => 'some_url'
  requestUrl => `$baseUrl\$request_path` # `to_s` is implicitly called on `request_path`

  Response fetch(Params params)
}

If/else if/else/finally

Problem: Sometimes when doing an if, we need to return but also perform an action that we also need to perform after the if, but we can’t do the action until after the condition is processed.

Eg: calling NextToken() in an if and return, and also calling NextToken() after the if statement.

Solution: add a finally block to an if statement Using a finally block for the if would allow us to only include the call to NextToken() once and also logically group it to that block of code.

LIKE: Implicit return values from blocks, methods, and other control structures

thing =
  case other_thing
  when Thing ; true
  when nil   ; false
  else      ; :ok
  end

LIKE: ifs at the end of the line

def x
  return 'yeah!' if true
end

DISLIKE: ifs at start of line require bulky syntax

if true then return 'noooo!' end

Would be nice to be able to do:

if true return 'yeah!'

This is possibly because we dont know where the if statment ends without parentheses... however we already have this problem when using blocks in certain contexts with methods without parentheses

# A new language based off of ruby
class X
accessor :foo, :bar # creates methods like `def a() @a`, `def a=(value) : @a = value`, `private self.@a`
# which naming is better?
reader :a, :b, :c
writer :y, :z
# I like this, and it's consistent with the
values format I like below
reader a, b, c
writer a, b, c
getter :a, :b, :c
setter :y, :z
#
# getting/setting default values
# which naming is better???
#
reader :x, -> { @x ??= "Hello" }
writer :x, ->(value) { @x = value }
reader x => @x ??= "Hello"
writer x(value) => @x = value
# this one is like the above, but the value is implicit rather than explicit
reader x => @x ??= "Hello"
writer x => @x = value
# this one is like the above, but showing how multiple accessors can be defined on a single line
reader x => @x ??= "Hello", y => @y ??= "Yello", foo, bar
writer x => @x = value ?? "Hello", y => @x = value ?? "Yello", foo, bar, baz
getter :x, -> { @x ??= "Hello" }
setter :x, ->(value) { @x = value }
# Notes: some ideas for shorthand for @x
reader x => @ ??= "Hello"
writer x => @ = value
reader x => attribute ??= "Hello"
writer x => attribute = value
reader x => attr ??= "Hello"
writer x => attr = value
get x => @ ??= "Hello"
set x => @ = value
x { get => @ ??= "Hello", set => @ = value }
x => @ ??= "Hello"
#
# multi-line reader/writer
# rather than mapping assignment to def x=, just continue to use reader/writer instead
#
reader x
@ ?? do_some_things
end
reader x
attribute ?? do_some_things
end
writer x
@ = do_some_things_with(value)
end
writer x
attribute = do_some_things_with(value)
end
#
# single line method definitions
#
# I like this because method is the same length as reader/writer
method x => @x ??= "Hello"
method x=(value) => @x = value ?? "Hello"
def x() @x ??= "Hello"
def x=(value) @x = value
# winner!
def x() => @x ??= "Hello"
def x=(value) => @x = value
def x() -> @x ??= "Hello"
def x=(value) -> @x = value
def x() : @x ??= "Hello"
def x=(value) : @x = value
def x ->() { @x ??= "Hello" }
def x= ->(value) { value }
#
# constructors
#
# set attributes directly using positional params
init(@a, @b, @c)
# setting attributes directly using named params
init(@x x:, @y y:, @z z:) # X.new(x: 1, y: 2, z: 3)
# or, would this be better?
init(x: @x, y: @y, z: @z) # X.new(x: 1, y: 2, z: 3)
# and then have optional/default values:
init(x: @x = 1, y: @y ||= 2, z: @z ??= :hello) # X.new(x: 1, y: 2, z: 3)
init()
#
# methods
#
def hello
do_some_stuff
# method overloading
def hello(a)
do_some_other_stuff
def hello(a = 7) # can't do this as the optional param makes it hide hello()
def another_method => do_another_thing
# if end is optional, it means we can't have nested classes and methods maybe just have optional ends for defs?
#
# syntactic sugar
#
i ||= 1 # sets i to 1 if i is nullable
i ??= 1 # sets i to 1 if i is empty
#
# default method parameter values
#
# option arguments - optional arguments must always go last
# a == 1 if a is not supplied
def method(a = 1)
# default if parameter is nullable - params are required for default values, optional arguments must go after
# a == 1 if a is nullable
def method(a ||= 1)
# default if parameter is empty - params are required for default values, optional arguments must go after
# a == 1 if a is empty
def method(a ??= 1)
# can also set instance variable
def method(@a = 1)
def method(@a ||= 1)
def method(@a ??= 1)
#
# nullable/boolean objects (trinary logic, trivalent, ternary, or trilean)
# can only be one of nil, true, false
#
NullPerson.new == nil # true
TruePerson.new == true # true
FalsePerson.new == false # true
#
# switch statements
# can we introduce some of the C# switch matching funcationality?
#
case var
when User u and u.id == 123 : u
when AccessToken t : t
end
# I also like the use of the parenthases for each case in this example inspired by bash:
case os
Linux) do_something_1
FreeBSD|OpenBSD) do_something_2
SunOS) do_something_3
*) do_default
end
end
# extendable methods
# methods can be extended instead of overridden
class A
def some_method
puts "before entension"
extension
puts "after extension"
end
# This method doesnt use the `extension` keyword
def some_other_method
puts "in some other method"
end
end
class B > A
extend some_method
puts "in extension"
end
extend some_other_method
puts "in extension"
end
end
B.new.some_method
# "before entension"
# "in extension"
# "after extension"
# if the base method has no `extension` keyword, no code can be inserted from the subclass
B.new.some_other_method
# "in some other method"
# decorators
example 1: decorators wrap objects
decorator A
# decorates an object with additional properties or methods
def m2 => 'hi!'
end
class B
def m1 => 'blah!'
end
c = A.decorate(B.new)
c.m1 # 'blah!'
c.m2 # 'hi!'
# refinements
example 1: refinements insert methods into the call chain of specific objects within a given context
refinement TemporaryPatch
decorate Hash
def my_refined_method => ''
end
end
class Z
using TemporaryPatch
def do_stuff => {}.my_refined_method
end
example 2: refinements can override methods
refinement TemporaryPatch
decorate A
def m1 => 'hi!'
end
end
class A
def m1 => 'hello'
end
class B
using TemporaryPatch
def m2 => A.new.m1 # 'hi!'
end
# class inheritance
C > B > A
v v v
+---+ +---+
| | | |
+---+ +---+
# static inheritance
static method inheritance/overrides
static property inheritance/overrides
static methods and properties can call the base method/property
class B < A
self.method()
# which one? or all?
base() # calls the current method on base?
base.method() # calls the specific method on base
base[A]() # calls a specific method from a specific ancestor -- does this allow multiple inheritance???
end
end
# multiple inheritance
class C < B, A, X, Y, Z
# inherited methods are overridden from right to left (eg B overrides A)
# method overriding occurs when method signatures match
# base methods of each class can be accessed like this:
def overridden_method
base[B]()
base[A]()
base[X]()
base[Y]()
base[Z]()
base[B].overridden_method
base[A].overridden_method
base[X].overridden_method
base[Y].overridden_method
base[Z].overridden_method
end
end
# interfaces and abstract classes
# generics (covariance and contravariance?)
# type conversion
converting a base class into a subclass, or a subclass into a sibbling subclass.
need a way to pass the additional required values
does this construct indicate that all subclasses are mearly wrappers around parent classes?
class Subclass
init(a, b, c)
super(a, b)
@c = c
# normal init stuff
end
init(from ParentClass, c)
this/self is the parent object
# a and b should already be set on parent class at this point
# set the additional attributes here
@c = c
end
end
Subclass.new(parentObj, c)
# higher order functions - passing methods to functions
example 1: passing methods in the same context
def m1 => 'hi!'
def m2(func) => func()
m2(&m1) # 'hi!'
m2(-> { 'hi!' }) # 'hi!'
m2(proc { 'hi!' }) # 'hi!'
example 2: calling methods on enumerated objects - not sure what the correct syntax should be
[1, 2, 3].map(&:to_s) # ['1', '2', '3']
example 3: recusive calls to the function
# self is the function
# not sure which syntax to use
f = (a, b) => a + b > 0 ? self(a - 1, b - 1) : 0
f = ->(a, b) a + b > 0 ? self(a - 1, b - 1) : 0
f = ->(a, b) { a + b > 0 ? self(a - 1, b - 1) : 0 }
# compiling and type checking
example 1:
def m1(b) => b.m2
m1(a)
in the above example, object `b` must have the `m2` method defined.
if object `a` does not have the `m2` method defined, a compile error is raised
example 2:
def m1(c)
c.m2
m3(c)
end
def m3(d) => d.m4
m1(a)
m3(b)
in the above example,
object `d` must have the `m4` method defined.
object `c` must have the `m4` and `m2` methods defined.
if object `a` does not have the `m4` and `m2` methods defined, a compile error is raised
if object `b` does not have the `m4` method defined, a compile error is raised
# Singular Piping for Enumerators
A method can return an enumerator, and enumerators can be chained
In ruby, and probably most languages, the first enumerator completes, then the second, then the third, and so on...
I would like the ability to pipe a value to the next enumerator per yield, rather than on completion of the iteration.
`break` in any chained enumerator could end the entire enumeration
`next` would move to the next item
`yield` would yield the value to the next iterator in the chain
So something like this would be handy:
```
[1, 2, 3, 4, 5] -> select {|v| v > 3 } -> each_with_index -> collect {|v, i| [v, i] } -> each {|v, i| puts v }
```
With this construct, the value of `1` gets puts before `2` gets processed by the chain
The `->` indicates the return value of the current enumerator will be yielded to the next enumerator in the chain
Alternatively, using the `|>`
```
[1, 2, 3, 4, 5] |> select {|v| v > 3 } |> each_with_index |> collect {|v, i| [v, i] } |> each {|v, i| puts v }
```
The `->` or `|>` operator enumerates an array/collection (or value?), passes it to the next method in the chain,
and waits for the method to complete. Once the method completes, the next element is passed to the next method in the chain,
and again waits. This process is continued until all elements have been enumerated.
In theory, each element could be processed in parallel!
The above would be equivalent to:
[1, 2, 3, 4, 5].each do |v1|
[v1].select do |v2|
if v > 8
[v2].each_with_index do |v3, i3|
[[v3, i3]].collect do |v4, i4|
[[v4, i4]].each do |v5, i5|
puts v5
end
[v4, i4]
end
[v3, i3]
end
end
v > 8
end
v1
end
The return type would be [1, 2, 3, 4, 5] because of the initial `each`
The return values are basically ignored... which possibly isnt the most intuitive behaviour
But at the same time, this is about processing the values in the initial enumerator... not returning or extracting values
Think of it like an ETL pipeline!
Additionally, it would be nice to have a construct where values are yielded from only the first iterator.
So rather than
```ruby
[1, 2, 3, 4, 5, 6].each do |v|
do_something(v)
do_another_thing(v)
end
```
you could do:
[1, 2, 3, 4, 5, 6].each -| do_something -| do_another_thing # [1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6].each -| do_something -| do_another_thing -| select {|v| v > 3 } # [1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6].each -| do_something -| select {|v| v > 3 } -| do_another_thing # [1, 2, 3, 4, 5, 6]
For the last two, the `select` doesn't have any effect as the return value is coming from the initial iterator
The `-|` is like a wall, the value from the iterator goes in, but the return value is ignored
Likewise, `break` will only break the chain to the last `-|`

From TypeScript Playground example for nominal typing

A nominal type system means that each type is unique and even if types have the same data you cannot assign across types.

TypeScript's type system is structural, which means if the type is shaped like a duck, it's a duck. If a goose has all the same attributes as a duck, then it also is a duck.

The shape concept mentioned above is a structural type system. Colloquially known as duck typing, this type of system works extremely well for processing and transforming structured or semi-structured data. It's used to great effect in Ruby, TypeScript, and many other languages.

I guess there is an overlap between discriminated union structures vs interface structures.

See Duck Typing in Practice, Part 2 for a Ruby example. Ruby is a dynamic language that doesn't have interfaces. In Ruby, any value can be passed in to any method. Duck typing of the params can be performed in a number of ways. Often a param is checked for the presence of a transformation method to convert the value into another value using respond_to?. Another approach uses case to check type or value and perfomr the transformation internally.

Nominative

See Nominal typing techniques in TypeScript for some great TypeScript examples of implementing a form of nominal tying.

See Nominative And Structural Typing.

  • Equivalence predicate: Given two type expressions T1 and T2, are T1 and T2 equivalent? In other words, are all objects of type T2 valid objects of type T1, and vice versa? Commonly written as T1 == T2.

  • Subtype predicate: Given two type expressions T1 and T2, is T1 a subtype of T2? In other words, are all objects of type T1 also objects of type T2? Commonly written as T1 <= T2.

    • Note that T1 <= T2 && T2 <= T1 implies that T1 == T2; type systems are a PartialOrder. (Can anyone name a type system that violates this common theorem?)
    • It is also useful to define a "strict subtype" predicate; wherein T1 < T2 iff T1 <= T2 and T1 != T2.

"does object X have method/attribute Y", as opposed to "is object X a subtype of type Z"

See Flow docs for Nominal & Structural Typing.

Thoughts

It would bbe nice to be able to switch between structural and nominal typing as needed.

I like the use of interfaces and being able to implement an interface from any instance or type would be very useful.

let myStr = "Hello World!"

interface Formatter {
    string format()
}

let myStrFormatter = myStr as Formatter {
    string format() {
        return "{this}!!!!!!!";
    }
}

string log(Formatter formatter) {
    console.log(formatter.format());
}

log(myStrFormatter);

// or 

log(myStr {
    string formatter() {
        return "{this} :)";
    }
})

I would also like to be able to use structural typing. E.g. to process CSV or JSON data or results from an SQL query.

I'd like to be able to switch on type and/or value.

I'd like to be able to either transform a value using a transformation method if it exists, or use the actual type. E.g. MyInt = int | { to_int }. Alternatively, the { to_int } part could be defined by an interface.

I'd like to use nominal typing for when I need to ensure two types are not compatible. E.g. USD vs AUD. Or user provided string vs application provided string vs sanitised string. These things need explicit conversions. But I dont want to have to add in extra primitive values in order for the type to be identified.

idea: no null values allowed

Can we avoid nulls in the language? Everything must have a default value?

List<string> names = null; // what if this is not allowed?
List<string> places; // what if this creates an empty List?
List<string> places = default; // what if this also creates an empty List?

idea: typed nulls or default values

Building on the idea that no null values are allowed, and everything must have a default value, we can build structures like this:

if (x is default) ...

But how is this different from x is null??

For starters, the default value would have default implementations of methods and properties. Is this useful though?

When iterating an enumerable type object, we often don't care that the enumerable is empty. We often just want to enumerate. We treat null equivalently to an empty enumerable.

E.g. in Ruby (x || []).select( |num| num.even?) or x ||= []

E.g. in C# we use the null coalesce operator.

When doing string manipluations, we often do not care that a string is null. We treat a null string equivalently to an empty string.

E.g. in C# string.IsNullOrWhitespace(x) or (x || "").Split()

This pattern is also described by the Null Object design pattern.

Much boilerplate code goes away when we implement the null object design pattern.

What would this look like in a language like Rust where there is no inheritance?

Ideally we shouldn't have null or undefined values.
I need to do research into how other languages acheive this.
It seems that variales of simple value types always have a default value even if not explicitly set. E.g. int = 0, bool = false, etc
Even structs can take on default values;
Could we acheive the same for reference types and complex value types?
```
class MyClass
{
default {
// provide a default implementation usinng anonymous class syntax
// override methods and properties to return values or throw exceptions etc.
}
}
MyClass variable = default;
public void MyFunction(MyClass myOptionalClass = default)
{
}
```
Can we also provide a way to define truthy, falsey, values or other types of comparisons?

This needs work

Using continuations, it should be possible to raise a request and have a handler intercept the request, execute some code, return back to the caller, and continue execution of the method.

The concept is similar to exceptions. When an exception is raised, the exception will bubble up to the exception handler. The exception handlers generally handle the exception or re-raise the excpetion.

Request handlers work similar to this too in that requests pause code exceution and bubble up and are then caught by a handler which then executes the handler code, however, at the end of the handler body the code flow is returned back to the original method.

In addition to this, the handler can return a value back to the caller.

Example

def do_something(succeed = true)
  request Start.new("Starting...")

  if succeed
    request Success.new("Succeeded")
  else
    request Fail.new("Failed")
always
  request Complete.new("Completed!")
end

def example
   do_something
   do_something(false)
handle Success
  puts `success handler: ${@.message}`
handle Fail
  puts `fail handler: ${@.message}`
handle Complete
  puts `complete handler: ${@.message}`
end

example
# success handler: Succeeded
# complete handler: Completed!
# fail handler: Failed
# complete handler: Completed!

Example returning a variable from a handler

The handler can also return a value:

def do_something
  val = request Request.new
  puts val
end

def example
  do_something
handle Request
  "hello world!"
end

example
# hello world!

There are a few confusing things about this code:

  • The request keyword is unintuitive
  • It feels odd that the code in the method block continues excecution after the handler completes
    • if this were a catch, the control would leave the method after the catch, but instead the control is returned to the method and continues execution
    • maybe this is because I am not used to this style... but it does twist my head

Update 2023-07-22

  • It may simply be that request is the wrong word
  • Confusion may be eliminated if the request also exits the function, however, what would/should the return value of the function be?
    • The developer shouldn't need to check the return value for null or some error state.
  • Is it an error state if a request isn't handled?
    • Should the compile display a warning or even an error?

The example above represents a workflow where events are emitted are different points in the workflow and can be responded to by other parts of the application.

Another context which may be applicable is success/error state. And indicating that a caller must (or should) handle all states. This can be represented by throwing, or returning a value that may represent a success or failure, or a tagged/discriminated union, etc.

In this case where the return value may be mutiple states, perhaps a different function syntax is needed. The need to state that this function will return multiple states, and those states must be handled.

fn do_something()
  if condition1; return new SomeError()
  if condition2; return new SomeOtherError()
  return "done"
end

fn handler()
  var x =
    handle do_something()
    when SomeError e: "oh no! {e.message}"
    when SomeOtherError e: "hmmmm.... {e.message}"
    default v: v
    end
    
  puts x
end

The fact we are return multiple types should be enough for the compiler to inform us we need handle keyword to handle all the states/values the method may return.

Idea: Code docs for multiple return statements

fn do_something()
  if condition1; return new SomeError() // error doc for SomeError
  if condition2
    // error doc for SomeOtherError 
    return new SomeOtherError()
  end
  
  if condition2
    // just a comment
    return new SomeOtherError() // this comment has higher precedence - or should precedence be reversed?
  end
  
  // doc for string return value
  return "done"
end

Problem:

  • What if we have multiple returns for the same type?? How do we document that?
    • accumulate all the docs?
    • docs above the function have the highest precedence?

Idea: defer handling

var x = defer some_method()

var y =
  handle x
  case ...
  default ...
  end

Problem: This introduces yet another way to pass around functions.... we want to be consistent and with this and not have multiple ways to do this

Type Constraints

  • Specifies the interface that a variable must have (eg must have methods do_stuff and foo)
  • Like an interface, but not explicitly implemented by the class/object
  • Exception is raised if a variable is passed in that doesn't contorm to the type constraint
  • Could define the type constraint inline, or in a similar way to a class/interface
  • Constraint could require must have this method with this signature (the method signature may refer to additional constraints)
  • constraint could require must implement this class/interface

Questions

  • How do you say enumerable of things that have method foo or enumerable of things that implement Bar
  • How do you say hash of things that has key with method to_s and value with method to_i

A Shape defines an interface that can be applied to an object that matches a given set of conditions.

Classes do not implement shapes, instead, classes can be cast to a shape when it matches the given set of conditions.

Defining an anonymous shape

The basic shape is the anonymous shape.

An anonymous shape has the following syntax:

Specifying a single method:

{ some_method }

Specifying multiple methods:

{ some_method, some_other_method }

Any object that has a method matching the methods defined in the anonymouse shape definition can be passed assigned.

QUESTION: How do we support overloaded methods using this style? Do we need to also specify the param types or even the return type????? This would be unweildy. Perhaps instead an type could be created that defines a method definition, and methods can implement that definition. eg like delegates from C#, except there can be a specific syntax to explicitly indicate that a method is using the delegate definition. If this was used, I'd want to replace the delegate keyword with a different more intuitive name. Like method definion or method signature. Interfaces and shape definitions could then reference the signature instead of specifying the actual method signature.

Using an anonymous shape for a method parameter

def function({ to_s } blah)
  ...
end

Using an anonymous shape for a generic class

class Something<{ thing }>
  def thing({ thing } param)
end

module MyThing
  def thing
    ...
  end
end

class MyClass extends Something<MyThing>
end

Using an anonymous shape with an alias for a generic class

The as keyword allows a shape to be given an alias.

class Something<{ thing } as Thing>
  def thing(Thing param)
end

module MyThing
  def thing
    ...
  end
end

class MyClass extends Something<MyThing>
end

Defining a named shape

A named shape is the same as an anonymous shape but has a few extra abilities

  • Has a name (in the same sense as a class name, module name, or interface name)
  • can define methods that the caller can call on the shape
  • can define additional conditions that can be met for the shape to apply to

Defining a named shape

shape PathProvider is String || { to_s } as to_s || { request_path }

  def path
    case @
    when String           : @
    when to_s             : @.to_s
    when { request_path } : @.request_path
    end
  end
  
  def to_s => path
end

def fetch(PathProvider path_provider)
  http.get(path_provider.path)
end

fetch("hey")
fetch(new Class { def request_path => 'hey' })

This feature is often called discriminated unions, however, the above implementation is better because a specific interface is defined for each type participating.

Here is an interesting article that discusses F#'s discriitated unions

Casting existing types to a shape

To improve the compatibility of this feature with existing types, types should be able to be cast to a shape.

This should be done decleratively. The definition can override the methods of the shape

shape MyShape
  def some_method
  end
end

# define outside a class
MyType cast to MyShape
  def my_method
    # add implementation for my_method
  end
end

# define within a class
class MyType
  cast to MyShape
    def my_method
      # add implementation for my_method
    end
  ned
end

In theory, this pattern could be used to cast any type to any other.

One thing is a bit off with this implementation is that cast to almost seems like it requires an interface rather than a type or a shape. However, I like that the instance itself remains. It is not returning a whole new instance of seomthing else, but is presenting the existing instance in a new way.

Further thoughts on Shapes

What is the problem we are trying to solve here?
We want to apply the same logic to two or more different types.
We only care that the object or primitive we are working is of an expected type.
We may not have control of the obejct definition.
We are extending the functionlaity of or applying to existing types.
A shape definition descried above is really no different from defining an interface.
The problem is in most languages, interfaces need to be explicitly implmented in the class definition.
What if we don't own the class? We need to create a wrapper or have some other mechanism of mapping the type to the interface. \

What if we were able to define an interface implementation for any class?
Our object could then be used wherever the interface is used.
If I want some logic to apply to multiple types, I create the interface I need to apply that logic, then I create interface implementations for each type I want to apply that logic to.

Here is an example to demonstrate the concept...

interface MyInterface
  def do_the_thing
end

# A method that utilises the MyInterface method
def my_method_using_my_interface(MyInterface interface)
  interface.do_the_thing
end

# Add MyInterface implementations for each type we want to support
implementation Dog > MyInterface
  def do_the_thing
    # some implementation
  end
end

implementation int > MyInterface
  def do_the_thing
    # some implementation
  end
end

implementation string > MyInterface
  def do_the_thing
    # some implementation
  end
end

implementation Circle > MyInterface
  def do_the_thing
    # some implementation
  end
end

# each object is automatically cast to MyInterface
my_method_using_my_interface(new Dog("Jessie"))
my_method_using_my_interface(1)
my_method_using_my_interface("Hello world")
my_method_using_my_interface(new Circle(7))
my_method_using_my_interface(new Square(7, 6)) # this fails as Square does not have an implementation for MyInterface

I think this concept is superior to the concept of discriminated unions, tagged unions, disjoint unions, etc.
This is because:

  • Existing language features are being used in better ways
  • A consistent interface is applied over the top of the disjointed types we want to support
  • If discriminated unions, tagged unions, disjoint unions, ect want to support more types, the definition needs to change to include the additional supported types. Whereas, by using interfaces, the interface definition doesn't change, we simply add implentations for the additional types we want to support. Thats a huge difference! Out interface remains stable!
  • An interface doesn't care about the inner workings of the types that implement it. Whereas discriminated unions, tagged unions, disjoint unions need to have understanding of the definitions of the types that are supported.
  • An implementation simply maps the type to the interface

Another example is from https://youtu.be/7YBwVoSn3P8

// A method that actually returns three responses, an exception, a movie, or null
public Movie? Update(Movie movie)
{
    movieValidator.ValidateMovie(movie); // may throw validation error
    var movieExists = movies.Contains(movie)
    if (!movieExists) return null; // return null if not found
    // more code to update movie
    return movie; // returns movie if successful
}

// descriminated unions version suggested in video
public OneOf<Movie, MovieNotFound, MovieValidationError> Update(Movie movie)
{
    var validationResult = movieValidator.ValidateMovie(movie);
    if (!validationResult.IsValid) return new MovieValidationError(validationResult);
    
    var movieExists = movies.Contains(movie)
    if (!movieExists) return new MovieNotFound(movie);
    
    // more code to update movie
    
    return movie;
}

// in controller action
var result = Update(movie);
return result.Match<IActionResult>(
    m => Ok(m.MapToResponse()),
    m => NotFound(m),
    failed => BadRequest(failed.MapToResponse())
);

Using interfaces and implementations:

public UpdateMovieResult Update(Movie movie)
{
    var validationResult = movieValidator.ValidateMovie(movie);
    if (!validationResult.IsValid) return validationResult;
    
    var movieExists = movies.Contains(movie)
    if (!movieExists) return new MovieNotFound(movie);
    
    // more code to update movie
    
    return movie;
}

public interface UpdateMovieResult
{
    bool UpdatedSuccessfully { get; };
    IActionResult MapToResponse(Controller controller);
}

public implementation Movie : UpdateMovieResult
{
    UpdatedSuccessfully => true;
    MapToResponse(Controller controller) => controller.Ok(this);
}

public implementation MovieValidationError : UpdateMovieResult
{
    UpdatedSuccessfully => false;
    MapToResponse(Controller controller) => controller.BadRequest(this.MapToProblemDetails());
}

public implementation MovieNotFound : UpdateMovieResult
{
    UpdatedSuccessfully => false;
    MapToResponse(Controller controller) => controller.NotFound(this.MapToProblemDetails());
}

// in controller action
var result = Update(movie);
var response = result.MapToResponse(this)
return response;

// alternatively, if our interface didn't have a MapToResponse() method we could use type switching
// this way is less preferable, as if new implementations of UpdateMovieResult were added,
// the switch statement would need to be updated
switch (result) {
    case Movie movie: return Ok(movie);
    case MovieValidationError error: return BadRequest(error.MapToProblemDetails());
    case MovieNotFound error: return NotFound(error.MapToProblemDetails());
    default: throw new Exception(!"Oh no! We haven't handled the {typeof(result)} implementation");
}

That's a fair bit more code than the OneOf example. So what makes this the better approach?

Firstly, the logic in each interface implementation is isolated and can be individually reused and tested.

UpdateMovieResult result = new MovieNotFound(new Movie());
Assert.False(result.UpdatedSuccessfully);

Second, if we need to add more return results, we dont need to modify the return value of the Update() method or any of the calling code. We simple create a new implementation of UpdateMovieResult and then return the new implementation from the Update method.

E.g. we might want to return a UserNotAuthorisedToUpdateMovie result. We can simply create an implementation of UpdateMovieResult for UserNotAuthorisedToUpdateMovie, then return an UserNotAuthorisedToUpdateMovie object if the user is not authorised.

The final benefit is we can cast any implementations of UpdateMovieResult back into the implementing types if we need to using pattern matching.

switch (result) {
    case Movie movie: /* do something */; break;
    case MovieValidationError error: /* do something */; break;
    case MovieNotFound error: /* do something */; break;
    default: throw new Exception(!"Oh no! We haven't handled the {typeof(result)} implementation");
}

Note that this isn't the ideal approach however. If an implementation is added, the switch statement will not handle that new implementation. The best approach is to utilise the interface by including a method that performs the required action. If the interface cant be used to perform the required action, this is an indicationn that the required abstraction has not been reached.

Problems:

  • In many languages, we only use await style constructs in an specially defined async style function.
    • In theory, we should be able await a function or block of statements from anywhere.
  • Async/await style constructs spread like a virus…
    • See: https://youtu.be/MoKe4zvtNzA
      • a reaction vid to "What colour is your function", about methods either need to be red or green… meaning we have to choose between async or not, and where we choose async, it must spread to everywhere that will call the method,
    • E.g. in C# await can only occur in asuc functions, otherwise we have to use alternate syntax such as calling functions like Wait and properties like Result or Exception on the returned Task object.
  • A task represents a running function (or a promise of a return value at some future time), that may or may not have completed yet.
    • The problem being:
      • the code is already executing
      • The resulting returned value is memoised, the task cannot be rerun.
    • This isn't quite a big deal, since a function can be passed around innstead of a task and called multiple times
  • Error handling works completely differently when not using an await style syntax, and instead returning a Task or promise type oject as a proxy for the eventual completed function call.
    • E.g. in C#, unless you use await, exceptions will not bubble up and cannot be caught by using try/catch syntax
      • Instead, the exception is accessible though an exception property, along with other properties representing the success or failure state.
        • The success/failure state should should probaly always be checked, but probably often isn’t.

Success/Error path

How can we provide a consistent way of representing the success and failure paths for function calls in both a synchronous and asyncronous context?

Some of the complaints about try/catch annd throw is that inn most languages, the error path is not documented and it is not easy to determine where the error path may be handled.

In Rust, this is handled with a ? operator, and an explicit Result return value. The drawback to this is the return value must either be always checked for success/failure, or the unhandled error value must propogate out of the function. This would result in a lot of code using switch or match style statements.

In Go, there is the concept of channels which could be used to represent success and error paths. I am not sure what drawbacks this may have. However, I imagine it would work quite similarly to callacks, except with the benefit that the function caller has control (rather than a callback), so could simply return, or continue performing some additional logic.

There are discussions around the use of tagged/discriminated unions as return types to represent error states. A return value could be any value allowed within the union. This has the same problem as Rust's Result; the return value must be checked for success/failure, or the unhandled error propogated out of the function. This would result in a lot of code using switch or match style statements.

There is also the functionality provided by continuations. Continuations have the same drawbacks as try/catch; the flow of code is not explicit and error states may be handled in other parts of the code from where an error was thrown. The benefit of continuations, however, is that they can be used to represent an unhandled state within a function that must be handled, otherwise, execution of the application will fail. This is discussed in request_handlers.md.

The drawbacks cited for try/catch and continuations regarding implicit control flow, in theory could/should bbe picked up by the compiler. In Java, exceptions must be declared in the method signature, but could just as easily be disoplayed in an IDE with the help of the compiler. Any unhandled exceptions or requests (in the case of continuations) could be raised as warnings, or even as errors. This is exactly what is expected of the compiler for any tagged/discriminated union implementation.

TODO: checkout Rust formatters. debug, vs others... doesnt seem like I can define my own though which is a shame

Nested strings

Problem: concatenation of a hierarchy of objects likely requires special logic to correctly format. Eg printing nodes in AST and correctly indenting.

Solution: Likely we need to a separate renderer rather than using ToString()

Problem: String format in different contexts. Eg debug vs production, localisation, what is rendering the string.

Solution: likely also requires a renderer. Don’t have a ToString() method at all.

Solution: ToString() takes an optional renderer object. Uses a default renderer if not supplied. Maybe use a default renderer?

ToString(Renderer renderer = default);

Problem: Doing anything complex in ToString() may require a StringBuilder or something else to build the string efficiently.

Solution: Renderer has access to a StringBuilder type class. Or some sort of string stream with helper methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment