ozra / onyx-lang

The Onyx Programming Language
Other
97 stars 5 forks source link

Suggestion: Non-nil suffix for parameters in function definitions #73

Open stugol opened 8 years ago

stugol commented 8 years ago

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.

ozra commented 8 years ago

Interesting. The opposite would be even better though..

stugol commented 8 years ago

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

ozra commented 8 years ago

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

stugol commented 8 years ago

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 commented 8 years ago

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:

ozra commented 8 years ago

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.

stugol commented 8 years ago

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?

ozra commented 8 years ago

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.

stugol commented 8 years ago

Perhaps:

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

Alternatively:

a String     -- mutable
b String!    -- immutable const
ozra commented 8 years ago

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.

stugol commented 8 years ago

? 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? ;)

ozra commented 8 years ago

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.

stugol commented 8 years ago

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?
ozra commented 8 years ago

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.

stugol commented 8 years ago

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?

ozra commented 8 years ago

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 commented 8 years ago

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