ahnfelt / funk Goto Github PK
View Code? Open in Web Editor NEWPROTOTYPE: A minimal scripting language - OOP via lambda functions and pattern matching
PROTOTYPE: A minimal scripting language - OOP via lambda functions and pattern matching
Currently the only way I found to send messages to self is like this:
:newDog {| barkPhrase | {
| Bark | system.Log(barkPhrase)
}}
:newChiwawa {
:parent newDog("yip!")
:self {
| Yap | self.Bark
| m | parent(m)
}
self
}
:aDog newDog("woof!")
:myChiwawa newChiwawa()
aDog.Bark
myChiwawa.Yap
Should Funk define a "self_" function on the prelude?
Here are some thoughts on a type system for Funk—I just took the examples from the slides and wrote some ideas about what their inferred types ought to be (and why). I’m just focusing on static types for now; incorporating dynamic/gradual typing would be a good next step after these details are figured out.
{|x y| x + y} : forall a b r. { "+" : a -> b | r } -> a -> b
y
is inferred as a fully polymorphic type a
; x
is also polymorphic, but constrained to accept a message +
which takes an argument of type a
and returns a result of type b
. The lambda just passes this message along to x
, so it also returns a b
.
The row polymorphism here (forall r. … { … | r } …
) indicates a record that may contain more fields (resp. an object that may accept more messages) than those that are explicitly indicated. This allows for a kind of subtyping: a “larger” type that accepts more messages than just +
can be passed for the x
parameter. In following examples, I’ll make this polymorphism implicit.
{|x| x + 1} : forall a. { "+" : number -> a } -> a
Here, x
is passed the message +
with an argument of type number
, so its type is constrained accordingly. The result type remains unconstrained. A more concrete type such as number
can be passed for the x
parameter, in which case the inferred type of this lambda would be constrained further, to number -> number
. I use lowercase number
instead of Number
to differentiate it from Funk’s treatment of uppercase barewords as string symbols, for reasons that will be clear in a moment. It could also be called num
or something if you prefer.
This type is also a generic instance of the more concrete type number -> number
, and the user might supply that as a type annotation.
{1} : forall a. a -> number
This function does nothing with its argument, so its input type is unconstrained, and it returns a number
.
fib := {
|0| 0
|1| 1
|n| fib(n - 1) + fib(n - 2)
} : number -> number
The patterns in fib
constrain its input to be of type number
; its results, 0
and 1
, also constrain its result to number
. This could be given a more general type if number literals were polymorphic (as in Haskell) but I’m skipping over that for the sake of simplicity.
color := {
|Red| "#ff0000"
|Green| "#00ff00"
|Blue| "#0000ff"
} : { Red : string, Green : string, Blue : string }
; : (Red -> string) /\ (Green -> string) /\ (Blue -> string)
; : (Red \/ Green \/ Blue) -> string
; : colorName -> string
Here there are a few different options. If Red
, Green
, and Blue
are taken as symbols with singleton types (that is, the type of the value Red
is the type Red
), then color
is inferred to have an “object” type which indicates the result types of the messages it responds to—here, all String
. This is essentially an intersection type (Red -> string) /\ (Green -> string) /\ (Blue -> string)
, denoting an “overloaded” function. (The /\
bikeshed could also be painted &
if you prefer.) Because the result types are all the same (string
), this can be simplified into a function that takes a union type: (Red \/ Green \/ Blue) -> string
or (Red | Green | Blue) -> string
. If the type Red \/ Green \/ Blue
is known by some other name such as colorName
, then it could be further simplified to colorName -> string
.
if := {
|True t _| t()
|False _ f| f()
} : forall a. { True : forall b. (null -> a) -> b -> a, False : forall c. c -> (null -> a) -> a }
; : forall a. (forall b. True -> (null -> a) -> b -> a) /\ (forall c. False -> c -> (null -> a) -> a)
; : forall a. (True -> (null -> a) -> (null -> a) -> a) /\ (False -> (null -> a) -> (null -> a) -> a)
; : forall a. (True \/ False) -> (null -> a) -> (null -> a) -> a
; : forall a. boolean -> (null -> a) -> (null -> a) -> a
if
is similar to color
, in that we can infer its type based on the messages it accepts, then simplify it to a more readable type. In the most general case, I think the unused branches could be given fully polymorphic types, but that would leave you with higher-rank quantifiers, which might be a pain to deal with. So instead, the intersection of the two function types can be simplified by unification, making both branches null -> a
in both the True
and False
cases. Then the intersection can be simplified into the union True \/ False
, a.k.a. boolean
(or bool
).
max := {|x y|
if (x > y) {
x
} {
y
}
} : forall a. { ">" : a -> boolean } -> a -> boolean
This should be self-explanatory based on previous examples.
when := {
|True body| body()
|False _|
} : { True : forall a. (null -> a) -> a, False : forall b. b -> null }
; : (forall a. True -> (null -> a) -> a) /\ (forall b. False -> b -> null }
; : (True -> (null -> null) -> null) /\ (False -> (null -> null) -> null }
; : (True \/ False) -> (null -> null) -> null
; : boolean -> (null -> null) -> null
Because the False
branch does nothing, its argument’s type is unconstrained, and (I presume) it returns null
. So when the intersection is simplified, the a
from the True
branch is unified with null
, and the b
from the False
branch is unified with null -> a
, giving the type boolean -> (null -> null) -> null
.
while := {|condition body|
when (condition()) {
body()
while(condition, body)
}
} : forall a. (null -> boolean) -> (null -> a) -> null
Here the type of the body’s result is left unconstrained, since it’s never used. The return type of while
is inferred as null
because that’s the return type of when
. (As an aside, for optimisation purposes, the compiler needs to recognise that the call to while
is in tail position.)
Now we get into the interesting stuff: objects!
vector := {|x y| {
|X| x
|Y| y
|Add v| vector(x + v.X, y + v.Y)
}}
The inferred type for this would be quite gnarly if allowed to be fully general. This is the best I could come up with:
forall a b addA addB.
; The types “a” and “b” of the fields must accept “+” messages…
(a <: { "+" : addA -> a }, b <: { "+" : addB -> b })
=> mu self. a -> b
; …that can accept the types of the fields “X” and “Y” of the value passed to “Add”.
-> { X : a, Y : b, Add : { X : addA, Y : addB } -> self }
This can’t easily be simplified, so a user-supplied type annotation would be needed in situations like this to produce a readable type:
number -> number -> mu self. { X : number, Y : number, Add : { X : number, Y : number } -> self }
The notation for recursive types could be spelled differently, too, like rec self
; or self
could implicitly refer to the nearest containing {…}
type. And type aliases could be introduced for convenience:
type vector = mu self. {
X : number ; NB. newline could be accepted where comma is expected
Y : number
Add : { X : number, Y : number } -> self
}
newVector : number -> number -> vector
Record union captures the notion of forwarding messages in the inheritance example:
type monster := {
Name : string
Hurt : number -> null
}
newMonster := {|name hitpoints|
|Name| name
|Hurt damage| hitpoints -= damage
}} : string -> number -> monster
type creeper := { Explode : number -> null } + monster
newCreeper := {
super := newMonster("Creeper", 80)
{
|Explode area|
area.NearbyMonsters.Each {|monster|
monster.Hurt(50)
}
|otherMethod|
super(otherMethod)
}
} : null -> creeper
The “sum types” example works out pretty neatly too:
getOrElse := {|option default|
option {
|None| default
|Some value| value
}
} : forall a. ({ None : a, Some : a -> a } -> a) -> a -> a
You could always add “real” sum & product types if you want, rather than encoding them like this—it might be better for producing good error messages and checking whether pattern matching is exhaustive.
Extensible Records with Scoped Labels is a good starting point for implementing extensible records—it’s the basis of the record system in PureScript and the effect system in Kitten. If you want to incorporate higher-rank types, lexi-lambda did a reference implementation of Complete and Easy Bidirectional Typechecking for Higher-rank Polymorphism; even if you don’t need higher-rank polymorphism, bidirectional typechecking is a good technique that tends to produce better error messages than vanilla Hindley–Milner.
That’s all I’ve got for now—let me know your thoughts. :)
Really liked the concept. It's quite close to something that has been bouncing on my head for quite some time (a decade :P)
I think I found an issue that I don't have the time right now to check.
Trying:
:a {
:b 1
{
|Add n| n + b
}
}
:c a()
system.SetText(c.Add(2))
Results on:
Unexpected argument: function(_X) { if(_X == "Add") { return function(_X) { var n_ = _X; return _A(_A(n_, "+"), b_); }; } throw "Unexpected argument: " + _X;}
While:
:a {
{
|Add n| n + 1
}
}
:c a()
system.SetText(c.Add(2))
Works.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.