-
-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consider turning Inko into a functional language #665
Comments
Another change we'd have to make: enum variants have static methods generated for them, such that |
Reading through gleam-lang/gleam#654 (comment), I agree with much of what is said there in regards to mapping modules to files being easier. This means no explicit |
If functions are scoped to modules instead of classes/types, re-opening them wouldn't make much sense, unless we allow This means |
Runtime/memory wise things would basically change like this: Classes are turned into modules, and no longer concern themselves with allocation sizes or fields. When allocating a type we don't pass the class, but the size in bytes, and an For implementing traits I'm not sure yet. If functions are scoped to modules, then implementing them for a type should expose them to the surrounding module. This can lead to naming conflicts if two different types in the same module implement the same trait, i.e:
|
Scoping can also pose a challenge for droppers and destructors. Droppers are called |
Hi @yorickpeterse . |
@yorickpeterse What do you mean by |
I specifically don't want to allow every character in module names, e.g.
What I meant is that the data structures used for classes internally are changed and renamed to work for modules, rather than coming up with something entirely new. In practise this comes down to renaming them, and removing some fields that may no longer be relevant. This is purely an internal/runtime thing though, not something user facing. |
@yorickpeterse I see, thank you for explaining.. Regarding imports and name conflicts, this might be of interest: https://narimiran.github.io/2019/07/01/nim-import.html |
@kuchta Thanks, I'll take a look :) |
Thinking out loud here: If we always store functions in modules, and we want to allow multiple trait implementations for different types, we need explicit modules, such that we can do something like this:
If each file translates to a module, then each type would need to go in its own file if we want to implement a trait for it, otherwise those trait's functions may conflict with the same implementation for a different type in the same file. An alternative is that files do translate to modules, we don't have explicit modules, but we allow defining functions "inside" types like so:
Here This does however result in a similar split to what we have now: functions meant to operate on a particular object (e.g. |
That last paragraph does reveal an interesting fact: if we ignore static methods, the way Inko types and methods are organized is basically just like a functional language with type classes, just with a different syntax and lacking first-class functions (something that likely would remain the case if I were to go along with the proposal of this issue). |
I think that using so much syntactic structure is a bit outdated. When I'm considering new language for small things to start with, I'm automaticly ruling out those with these properties. This problem could be solved by not using this identifier type imports (but disambiguation by full signature which you probably need anyway, if you want to support meaningfull UFCS) , as is mentioned in the above link. |
I'm going to shelve this idea for the time being, seeing as how the changes wouldn't bring that many benefits after all while at the same time being highly disruptive for existing code and users. |
Similar to #596, this is largely me thinking out loud, and to create a single place to gather notes.
Inko is currently mostly object-oriented. I say mostly, because we don't have message passing for regular objects (they're just method calls), no intercepting of messages, no inheritance, etc. Instead, we have objects with associated functions (= methods), and traits for composition, which basically "copy paste" methods into the objects.
A problem with our current setup is that expressing certain patterns requires a more complex/complicated type system, which I would like to avoid. A simple example is wanting to turn an
Option[ref T]
intoOption[T]
. This would require something like this:The type-system doesn't support this as traits are implemented for classes, not arbitrary types. Supporting this requires extensive complex changes, likely affecting compile times and possibly resulting in ambiguous method lookups.
In contrast, using a standalone function/module method we can express this:
When reopening classes we wouldn't need support for
impl ... if
either, as instead of this:We can just do this:
In addition, function signatures no longer need to use
fn mut
and instead just usemut
for the appropriate arguments. This makes async functions a little less confusing, as one can just writefn pub async foo { ... }
instead offn pub async mut { ... }
and probably not remembering if it'sfn pub async mut
orfn pub mut async
.Another benefit is consistency: in the current setup it's not always clear if something should be an instance method on a dedicated type, a static method, or a module method. Combined with the type system limitations this leads to some inconsistencies, such as
String.join
being a static method butIter.to_array
being an instance method. This can also lead to throwaway objects that only exist for the purpose of grouping methods together, something quite pervasive in typical OO languages.We'd also be able to change the field syntax from
@name
tovalue.name
, as we'd no longer have the ambiguity that arises when a method and field exist with the same name.One change we'd need to make is that processes need to be typed differently: the public facing type should be
async T
, while the interior bit (available only to async functions) would be typed asref T
ormut T
accordingly. This way we don't need to rely on the current type private hack for regular process methods.What I'm not sure about is how we'd handle modules, importing them, and types with the same name as modules. Take this hypothetical syntax for example:
How do I import the module
User
vs the typeUser
?import user.(User, User)
followed by some clever compiler choice which symbol to use (e.g. the type in signatures, the module elsewhere)?import user.(User as UserMod, User)
? Or maybe you just importuser.User
and the compiler figures it out for you? I'm not sure yet what the best choice is here.A critical part of this change would be support for uniform function call syntax, i.e.
value.function(arg)
translates intofunction(value, arg)
. This is better for code completion, and removes the need for a dedicated pipelining operator.I'm also not sure if/how we'd make functions first-class values. To make this happen we'd have to be able to refer to them, which conflicts with the call syntax allowing you to omit the use of parentheses. In addition, this would require heap allocating some sort of object such that an argument typed as
fn -> X
can receive both a function and a closure transparently, unless we want to specialize over that as well (likely complicating the specialization pass quite a bit).Purity and immutability wouldn't be a goal or even desired, i.e.
mut
stays and the mutability semantics remain unchanged; it's largely a syntactical change, and a change in how functions are organized.Long story short, this is something that I've been thinking about more and more over time, so I think it's time to start collecting proper notes.
Related links
The text was updated successfully, but these errors were encountered: