ozra / onyx-lang

The Onyx Programming Language
Other
95 stars 5 forks source link

Request: records inside functions, and typed records #59

Open stugol opened 8 years ago

stugol commented 8 years ago

Crystal doesn't allow records to be declared inside a function:

def get-fred
   record Person, name, number          # error!
   Person.new "Fred", 1
end

This is because record is a macro that generates a class, and you can't have classes inside functions. However, strikes me that your #49 visitor pattern lets us create anonymous classes wherever we want, right?

You also can't specify types for record members in Crystal. I propose we solve these two problems at a stroke:

get-fred ->
   type Person
      attr_reader :name, :number
      init(@name String, @number Int) ->
   Person.new "Fred", 1
end

Of course, we'll want to simplify it a bit. Such ad-hoc types are essentially records, and all their fields should be public. So we can have a terse syntax such as:

get-fred ->
   rec Person
      name String
      number Int
   Person.new "Fred", 1
end

This will presumably compile to an anonymous class at file-scope:

type ___anon_rec_Person
   attr_reader :name, :number
   init(@name String, @number Int) ->

get-fred ->
   ___anon_rec_Person.new "Fred", 1

Propose also mutable rec Person, generating a class where all the fields are read-write (attr_accessor instead of attr_reader). Mutability should be discouraged unless required, so making the default immutable makes sense.

stugol commented 8 years ago

Could even support a terser syntax, that compiles to the same thing:

get-fred ->
   rec Person
      name = "Fred"
      number = 1

The types of name and number can be inferred if not specified, and the same code generated as if the values were specified outside the record definition:

get-fred ->
   rec Person
      name
      number
   Person.new "Fred", 1
stugol commented 8 years ago

No error should result in the compound case:

get-fred ->
   rec Person
      name = "Fred"
      number = 1
   Person.new "Fred", 1

If all the values are specified, as above, rec should return an instance. In either case, the values should be considered defaults and can be overwritten:

get-fred ->
   rec Person
      name = "Fred"
      number
   Person(number: 1)
get-two-people ->
   value1 = rec Person
      name = "Fred"
      number = 1
   [value1, Person("John", 2)]

If any values remain unspecified, rec should return void.

   value1 = rec Person        -- error!
      name = "Fred"
      number
stugol commented 8 years ago

Before you ask: The real-world application for this is simply that returning an anonymous tuple is bad design:

my-fn ->
   {"Fred", 1, true, 27, #green}          -- WTF!? What do these signify exactly?

It's data without context. This is bad. It's the same reason we name constants in C++.

ozra commented 8 years ago

I totally agree it should be possible, not only to define types, but also functions, withing functions. I've been thinking about that since I started, glad you reminded me!

Let's break this down a bit:

With regard to the rec (which in that case should definitely be called record, which then of course clashes with the stdlib-macro from crystal... but), the path chosen atm for type is a bit "opposite" to that, for example: enum Something in Onyx (currently!) is type Something < enum, that is, all type definition follows ("type" TYPE_NAME "<" TYPE_BUILDER BASE_TYPE? [simplified]). A record out of that perspective would be type MyRecord < record - which no doubt looks a bit ugly and off-beat for such an "inline code"-construct.

I think the best solution here might be to use the crystal stdlib macro, but change the behaviour so that types can be defined within functions (we all want that).

I also think (technical detail) the temp-name should derive more data from code - making debugging easier:

type __anon_type__get_fred__Person
   name Str  'get
   number Int  'get
   init(@name, @number) ->

get-fred() ->
   __anon_type__get_fred__Person "Fred", 1

Notes for above:

I agree that having values' meaning specified is better, however with return values like in the example it's harder in a way: You can't easily specify that type outside of the function, if you'd need to (well, typeof() could be used). A future tuple with named elements could come in handy there - but this is just a "side-rant", not really important.

my-fn() ->
   <name: "Fred", number: 1, wtf: true, age: 27, fave-colour: #green> 

On the suggestion:

   value1 = rec Person
      name = "Fred"
      number = 1
get-two-people() ->
   record Person
      name = "Fred"
      number = 1
   [Person(), Person("John", 2)]

Another note: untyped inst-vars is not allowed. You've read the discussion regarding this in crystal issues I presume. This is not a major problem since simply assigning defaults like in your example infers it. I'm eager to refactor onyx-compiler into a daemon when the language has settled. It can then re-compile only changed parts and you could basically have a fully compiled program at each save in your editor with minimal CPU-cost. That will rock. (daemon-mode will be optional!)

Some current problems

With regard to some of the ideas discussed above: macros are used. There is one major thing still lacking in Onyx, and that is the macro/template handling:

So anything involving macros should be treated as twilight zone atm. I'm confident I'll solve this, and I'm currently coding on the parts required to get this working seamlessly - actual syntax for Onyx coming in last - it should sit well. (This is why nothing's "happening" on issues atm - this takes some effort)

stugol commented 8 years ago

the path chosen atm for type is a bit "opposite" to that

I don't follow.

I also think the temp-name should derive more data from code - making debugging easier

Of course. My naming was merely an example.

it's recommended to not use new in Onyx

Noted.

The recommended way of creating getters and setters in Onyx are via the inst-var pragmas 'get and 'set - this keeps inst-var names spatially aligned to the left.

I see. So if we want the object to be mutable, we simply add 'set to the variables?

I'm still considering requring @ before names of inst-var definition

You should. Except in a record, where you're conceptually defining a structure, not a class.

with return values like in the example it's harder in a way: You can't easily specify that type outside of the function, if you'd need to

Why not simply make a record be a labelled tuple? I define it with rec because I want to explicitly specify the types of its members - and their mutability - but the eventual object is a subclass of "labelled tuple".

If it's a one-off instance, then anon-type-style should be used instead.

You mean <~?

Another note: untyped inst-vars is not allowed. You've read the discussion regarding this in crystal issues I presume.

I have not, but it makes sense.

I'm eager to refactor onyx-compiler into a daemon when the language has settled.

Sounds useful.

ozra commented 8 years ago
  1. Regarding "opposite": Instead of having more different keywords for defining types, Onyx uses type only, and the type builder modifier for explicit diversions (like enum: type MyEnum < enum, which is like the "opposite" of going with a record keyword. But it's use-case is different enough that a diversion would be in order if it would be seen fit.
  2. mutable instvar: yes.
  3. @-prefix: As long as it's still just a macro they're not needed, and if a keyword would be deemed better for some reason, it's already diverging from the type syntax, so that would be a good idea then.
  4. record -> tagged tuple. That's definitely an option. It's best then to further discuss it "crystal-wide", since it will affect the AST for both langs, should CR implement it differently later on if it's put in Onyx now. It sounds interesting.
  5. anon-type-style: Exactly!