The Hylo
language was previously known as Val
.
Lots to like here. This is a language that embraces mutable value semantics. It's still in a pretty early state, without having a fully working compiler yet.
The ownership mechanism they use looks pretty interesting.
It feels as if I would still want to have "escape hatches" such that mechanisms other than value semantics are available. It will also take more thought on how to write large systems using mostly value semantics would work.
The value semantics and ownership models used seem very amenable to targets other than CPU.
;
and disambiguating some other scenariosdefer
does have orderingin
, inout
, sink
and set
:
)A module may also define an ABI resilience boundary, within which code and details such as type layout are never encoded into other compiled modules (e.g. via inlining). source
Nice to have the control.
Some operations are said to be consuming, because they force-end the lifetime of a binding. In other words, they must be the last use of the consumed binding. For example, assigning into a var binding consumes the source of the assignment. Similarly, tuple initialization consumes the source values. source
Huh - very linear typey. Note that here though the example is operating on float types that are immutable, and yet they are still "consumed".
I quite like the use of &
to indicate use mutability.
Does use .N
to access tuple members. Whilst a little odd seems better than zigs use of .@"N"
syntax. I suppose there might be some ambiguity on seeing N.1.0
, that the .1.0
, is it two float literals? If we don't know what N is then that could be a problem.
public fun main() {
let numbers = [0, 1, 2, 3, 4]
print(numbers[2 ..< 4]) // [2, 3]
}
Creates a slice. The 2 ..< 4
syntax is a little unusual. It does make more apparent (than say ...
) that the range is non-inclusive of the last index.
Has Optional<Optional<T>>
distinct from Optional<T>
elsewhere this has been described as being the superior arrangement.
Has argument labels a bit like objective C (and probably swift). Hylo doesn't support type based function overloading, because of this labelling mechanism.
Requires returned values from functions to be used by default. Has 4 styles of parameter passing to functions
let
- notionally passed by value. Immutable.inout
- notionally passed by "move-in/move-out" (although typically isn't actually)
dtor
as long as reconstructed before leaving function(!)sink
- indicates a transfer of ownership. A "pass by move".set
- lets a callee initialize an uninitialized value (alkin to C++ placement new)To create a copy requires an explicit .copy()
The let convention does not transfer ownership of the argument to the callee, meaning, for example, that without first copying it, a let parameter can't be returned, or stored anywhere that outlives the call.
Has methods and uses self
as the this
equivalent. Does a C++ type thing of placing the modifiers around this after the method.
Method bundles allow for multiple implementations, depending on the parameter passing style for self
. This looks kinda interesting and deserves more thought.
Supports closures, it is necessary to describe certain types of binding captures.
A subscript is a resuable piece of code that yields the value of an object, or part thereof. It operates very similarly to a function, but rather than returning a value to its caller, it temporarily yields control for the caller to access the yielded value. source
Called with '[]' not with '()' as with functions.
It appears the 'yield'ing is a little like continuations. The yield
returns a result to the caller, but the rest of the subscript is still executed when scope is left, at least that's how it appears in the example
subscript min(_ x: Int, _ y: Int): Int {
print("enter")
yield if y < x { y } else { x }
print("leave")
}
public fun main() {
let one = 1
let two = 2
let z = min[one, two] // enter
print(z) // 1
// leave
}
Subscripts can be methods, which can be named or not. Also allows bundles for differnent styles.
Adds yielded
as a way or marking parameters, where the usage style can be infered.
The computed properties section is interesting
type Angle {
public var radians: Double
public memberwise init
public property degrees: Double {
let {
radians * 180.0 / Double.pi
}
inout {
var d = radians * 180.0 / Double.pi
yield &d
radians = d * Double.pi / 180.0
}
set {
radians = new_value * Double.pi / 180.0
}
}
}
The property is a "subscript" "method bundle". For the inout
version, d is calculated in degrees, and yielded (returned) and the new value set (after the yield is returned). Does this mean the d after the yield becomes the in
part?
Interesting that can use back ticked names as identifiers, allowing the use of keywords as identifiers. The perhaps nice thing about this, is that might relieve some anxiety around using keywords the user might want to use.
A trait is a collection of requirements on a type. Note: A trait is not a type but it may form a type if it is part of an existential type.
Example...
trait Shape {
static fun name() -> String
fun draw(to: inout Canvas)
}
Has associated type, which can specify size requirements as well as other other type constraints
trait Generator {
type Element: Copyable
inout fun next() -> (element: Element, done: Bool)
}
Has extension
but not yet entirely clear how that works.
Similarly has conformance
keyword which appears to be an implementation of a conformance. It appears possible to make conformances conditional with where
clauses.
Has some surprising rules around conformance and traits...
import M
public type A {}
conformance A: M.T {} // OK: conformance is private
type B: M.T {} // OK: 'B' is not exposed outside of the module
public type C: M.T {} // error: cannot expose conformance to imported trait 'M.T'
Not clear why C
s conformance being public is not possible.
fun borrow<A>(_ thing: inout T) {
let a = [thing] // lifetime of 'thing' ends here
print(a)
thing = a.remove_last() // new lifetime of 'thing' starts here.
}
The lifetime of thing ends when a is initialized because construction of an array literal consumes the literal's elements. A new lifetime starts before borrow returns as the call to Array.remove_last produces an independent value.
Feels linear type-like.
subscript min<T, E>(_ a: T, _ b: T, by comparator: [E](T, T) -> Bool): Int {
let { yield if comparator(a, b) { a } else { b } }
inout { yield &(if comparator(a, b) { a } else { b }) }
sink { return if comparator(a, b) { a } else { b } }
set { if comparator(a, b) { a = new_value } else { b = new_value } }
}
The subscript example shows a generator in an array
extension Array where Element: Copyable {
subscript generator(from start: Int): var [some _] () inout -> Maybe<Element> {
fun[let self, sink var i = start]() {
if i < self.count() {
defer { i+= 1 }
return self[i].copy()
} else {
return nil
}
}
}
}
Needs more thought into why this is the way it is... quite alot going on there.
Looks like it has a way to define precedence. Interestingly it's not at the level of operators, but operator types.
Interesting addition of where
clause to for loop. It's appeal is perhaps not having to have if ... continue
as first statement. In example given can also filter on type.
Use of lower_snake
for names of things (like make_iterator
), hmm not keen.
Like swift uses prefix
, infix
and postfix
to distinguish operator types, without the need for C++ odd extra parameter hack.
Read Language Spec
From the related paper
Swiftlet applies copy-on-write on arrays only, as structures are allocated inline, enabling a different set of optimizations to elimitate unnecessary copies. One limitation of our approach, though, stems from its interaction with the implementation of inout arguments (Section 5). Recall that an inout argument is passed as a (possibly interior) pointer. Hence, the callee has no way to determine whether or not that pointer refers to a value inside of a shared buffer. As a result, the caller is compelled to copy non-unique storage defensively.
If the reference count is 1, but is contained in another array that is shared, if we allowed mutation of the contained value, we would mutate the contents of the outer array. To work around, the inner array has to be defensively copied. It will be unique/non shared, but that copy could be costly.
In the performance analysis swiftlet is slower than swift. It is argued that the majority of that is around swift having optimization passes that simplify the reference counting for copy on write. If the compiler can show an array is unique, it is not necessary to check the reference count every iteration.
This is interesting in that implies either some generalized way to indicate this behavior to the compiler (as a trait, detection by heuristic) or that it works for some special types (say a built in array type). The former seems preferable.
It is perhaps worth thinking about how reference counting might work in a multi-threaded environment. Thankfully the value semantics can be helpful. We'd prefer reference counting to not be atomic in general, because of the considerable increased costs, and reduced optimization opportunities (the compiler can't reason about what another thread might do in general). The language Nim takes this stance. We can add a function (generated at compile time), that when given a type can produce a copy that is guarenteed to be unique. It can then be passed between threads. Alternatively if a value is being "given up" to another thread it can be moved.