onyx-lang icon indicating copy to clipboard operation
onyx-lang copied to clipboard

Suggestion: Non-nil suffix for parameters in function definitions

Open stugol opened this issue 9 years ago • 17 comments

I suggest a ! suffix for variables and parameters, meaning "never infer a nil type":

fn(x!) ->
   ...

fn "text"     -- fine, x is String
fn 7          -- fine, x is Int
fn nil        -- compile error

In this example, the function is inferred as fn(x : String | Int), and the nil call is rejected.

stugol avatar Mar 29 '16 21:03 stugol

Interesting. The opposite would be even better though..

ozra avatar Mar 29 '16 21:03 ozra

You mean default to non-nil? In which case, the suffix should probably be ?.

stugol avatar Mar 29 '16 21:03 stugol

Yeah. And perhaps a slightly more formal "it has to do with the type"-notation should be used, it won't cost more than two chars in exchange for semantic clarity. Con being that it's uglier, but that can be a pro, depending on how often nils are to be expected:

foo(x, y) ->
foo(x *, y *) ->   -- equivalent to above
foo(x *?, y *?) ->  -- also accept nils

* means match any type (BUT Nil, according to the altered proposition), *? would mean match any type including Nil. Maybe you'll think it's regexp! :-P

ozra avatar Mar 30 '16 13:03 ozra

lol, it does look a bit like a regex. But I reckon it's serviceable. After all, String? means String | Nil anyway, so it makes sense.

Go for it.

What about variables?

a = 1
a = "Fred"
a = nil         -- error, `a` cannot be inferred as nil

b *? = nil      -- acceptable

I'm not convinced about that one, to be honest. Perhaps stick to function parameters (and global variables?) for this limitation.

Or, what might be better, do it for everything but allow the following shorthand:

b? = nil         -- equivalent to b *? = nil

stugol avatar Mar 30 '16 16:03 stugol

Similarly:

b = 7 if condition      -- error, nil possible here

b? = nil                -- this reference to nil is valid, but DOES NOT solve the error above

b? = 7 if condition     -- ok

So the shorthand is not so much "b may be nil", as "b may be set to nil HERE":

fn1(a *?) ->
  a = 4
say fn1 nil     -- "4"

fn2(a *?) ->
  a = nil
say fn2 nil     -- "nil"

fn3(a?) ->
   a = 4
say fn3 nil     -- "4"

fn3(a?) ->
   a = nil      -- COMPILE ERROR!

So, I guess we have two separate mechanisms here:

  • The type of a variable may be inferred as containing nil if the type is specified as *? (obviously if the type is specified any more explicitly, e.g. String?, there is no inference anyway)
  • A variable may also be inferred as nil if it is assigned to with a ? suffix (e.g. x? = nil)
  • Otherwise, a variable may not be nil.

stugol avatar Mar 30 '16 16:03 stugol

Currently local vars can't be type restricted, there is syntax implemented or it, but no semantics. For them a type-prefix is required, since the parser doesn't know i you're calling a function with a type as a parameter or type annotating otherwise. So, although not yet officially supported (it will cause problems with crystal<>onyx-macro atm, which I'm still working on):

a `TypeHere

Currently (these are also syntax constructs implemented since the start of Onyx, but with no real function yet - be aware) there are three type prefixes:

a 'Type  -- generic type prefix - auto according to context (mutable most likely)
b ^Type  -- explicit const / "let" variable
c ~Type  -- explicit mutable

But as said, those symbols are up for thinking about, and perhaps it's better with mut, auto and - hmm, forgot the last one atm, let's say const for the second, (they are also implemented as these alphanum alternatives apart from the symbols). But, once again, these are very loose ghost implementations atm.

In any event, if fully implemented, type restricting a local var will need a prefix of some kind (most likely ', since it's only used for pragmas otherwise, and they always begin with lowercase, which clearly distinguish them). ^ and ~ as operators currently has "Haskell bitwise syntax" .^. and .~.. Which perhaps should be changed back, making it impossible to use those for type purposes.

Also, local vars should default to allowing nil, only the "boundaries" (parameters and return value of funcs) should be stricter to create "barriers of safety", imo.

ozra avatar Mar 31 '16 12:03 ozra

If we have explicit mutable, doesn't that imply we also have implicit immutable?

I think bitwise operators are so rarely used that it makes sense to free up ^ and ~ for other purposes.

Do you consider immutable == const, or not?

stugol avatar Mar 31 '16 14:03 stugol

If we have explicit mutable, doesn't that imply we also have implicit immutable?

Depends on the context. I have some ideas for "code modes" for the future. You can choose stricter code modes, much like "use strict" etc. in some languages, but more choices. For instance "pure funcs by default", "immutable data by default". Naturally it's something that has to be discussed at length so that it turns out right and doesn't become confusing. Perhaps erroring if functions haven't been marked pure in the scope where the demand is put, so that the demand is upheld, and it's clear without looking at the top of the file, etc. It simply becomes a help in not missing explicitly adding demands per function. Anyway, this is out of the scope of this particular issue, I haven't created one or it yet, since there are more important things to get done first. But these are loose ideas for future developments.

I think bitwise operators are so rarely used that it makes sense to free up ^ and ~ for other purposes.

Yeah, that was my thinking when "ofuscating" them. Of course, they can be overridden for other purposes, but: 1. I haven't seen that happen often (at all?) for these two. 2. It's not good practice to override operators for non-obvious uses anyway. So, I think we're in the clear there.

Do you consider immutable == const, or not?

This is a tricky one - depends on what you attribute to the words really. Questions worth considering: should the symbol be re-assignable, or set-once? Only "contents" immutable? Etc.

I think the C/C++ way is unnecessarily complicated, causing more confusion then help.

The best way imo is all-in mutable and all-in immutable: "immutable-whatever-it's-called" means the symbol is set-once for the context and immutable all the way. If mutable, the symbol can also be re-assigned. This means there's a safety as to the meaning of a symbol in a context if chosen to be "locked". Further assigns has to be done to new symbols. In final binary code all intermediary variables are optimized away by LLVM, so there's no cost in stack usage.

ozra avatar Mar 31 '16 14:03 ozra

Perhaps:

a  = 4        -- mutable
b! = 4        -- immutable const

Alternatively:

a String     -- mutable
b String!    -- immutable const

stugol avatar Mar 31 '16 14:03 stugol

Still need a type-prefix, if local vars, otherwise it's a(String)... And ?/! has strong connotations relating to nilable/non-nilable so I don't think it's wise to confuse with mutability/immutability.

ozra avatar Mar 31 '16 15:03 ozra

? has connotations, but ! does not:

a String      -- not implicitly nilable
b String?     -- implicitly nilable
a? = nil      -- explicit nil assignment

c 'String!    -- immutable const String

d '*!?        -- immutable const nilable

I see no contradiction.

One has to wonder, however....precisely what value does a const variable have before assignment? And how can you then assign it?

For that matter, what value does a non-implicitly-nilable variable have before assignment? ;)

stugol avatar Mar 31 '16 16:03 stugol

Just thinking, all "throws" (well "raise"s) methods use exclamation mark, that's the non-nil connotation I was thinking about.

I like the idea of suffixing those concepts though, instead of changing ' - great idea!

An immutable would have no value until assigned - it will not be allowed to be used before set. It can definitely be allowed to be declared before setting, but it could as well be required to be set in one expression too, since you can simply use a conditional expression block to assign it. So I think that would be the cleanest and clearest solution.

Same then for required explicitly nilable.

ozra avatar Apr 01 '16 07:04 ozra

Just thinking, all "throws" (well "raise"s) methods use exclamation mark

I don't see what exceptions have to do with it. Isn't it just that the method doesn't return a value?

fn ->!
   4

say fn     -- nil

it will not be allowed to be used before set

Difficult to enforce:

a 'String             -- non-nil
a = "Nope" if false
say a                 -- error?

callback = ~> a       -- error here?
callback.()           -- error here?

(1..10).each (n) ~>
   a = n if n > 8
   say a              -- error?

stugol avatar Apr 01 '16 10:04 stugol

I don't see what exceptions have to do with it.

I was referring to stdlib-functions, sorry or the confusion, you raised yet a point to think about though.

say a -- error?

Yep. Error. a can be String | Nil at that point (or to be precise: it can only be Nil at that point), which is not allowed according to the restriction.

ozra avatar Apr 01 '16 11:04 ozra

What about the other lines? The callback, for instance? Do they error also? Specifically, does the error occur when the callback is declared, or when it is called?

stugol avatar Apr 01 '16 13:04 stugol

Yes, that's why it should probably be required that it's assigned in one expression (whether branched or not), ie the incomplete branch assign is not allowed to begin with. In any event it should be an error immediately when referenced before being guaranteed non-nil.

ozra avatar Apr 04 '16 20:04 ozra

Took the liberty of changing the title slightly, have mistaken the issue when searching a couple of times.

ozra avatar Sep 24 '16 19:09 ozra