Code Monkey home page Code Monkey logo

Comments (10)

titzer avatar titzer commented on June 9, 2024 1

One possible migration path would be to introduce the option type constructor first, while keeping the nullability of class and array types as it is now. The option type constructor applied to type T creates a type that represents the union of the null value with the set of values represented by the type T.

For syntax, I like postfix ? for types, so they look like T?, such as int?, Foo?, and so on.
This type constructor would have a couple of special cases, like T?? == T?, because of the union semantics above. Similarly, for a class or array type T, initially T? == T.

Overall, this kind of change requires the compiler to support the union of the old language behavior and the new language behavior, selectable with a command-line flag. (Similar to how I did the -legacy-cast migration), but bigger. It takes at least two bootstraps and stable releases to get the behavior on-by-default, because the compiler must first support the new semantics under a flag, then the compiler (and all tests) need to be migrated to comply to the new rules, then the flag can be flipped, then the flag removed.

To support the union of behaviors I would first introduce non-nullable types in the compiler's representation of Virgil types. They wouldn't necessarily have a source syntax, but it would allow working on the verifier rules. Thus the verifier could be incrementally migrated to generate errors, enabled by the command-line flag.

from virgil.

titzer avatar titzer commented on June 9, 2024

I'd like to incrementally move away from nullable types, perhaps introducing ? as the option type constructor, and null as the value for not-present. My concern is how much real code will end up with ? everywhere for nullable types and how often programs end up using null-tolerant constructs such as .?.

In Virgil, functions can be null. I've recently started to feel like having a optional-invoke construct would be useful, e.g. for invoking debugging/tracing code, like:

var onEvent: E -> ();

def myfunc(e: E) {
  onEvent?(e);
  realWork(e);
}

from virgil.

k-sareen avatar k-sareen commented on June 9, 2024

My concern is how much real code will end up with ? everywhere for nullable types

Yeah. In my subjective experience with Rust, it can litter the code with ? everywhere if one function deep in the call stack returns an Option (or Result) type, as you have to pass the values through the callstack until a function has enough context to deal with the None (or Err) value. Though often it is just the function 1 level higher (i.e. the caller function) than the function that is returning the optional value.

I've recently started to feel like having a optional-invoke construct would be useful, e.g. for invoking debugging/tracing code

Feels like ? is starting to get very overloaded then if it will be used for checking if a variable is None or Err (as Rust uses it), as the "optional-invoke construct", and also as the type query operator (.?). The optional-invoke construct definitely sounds quite interesting for benchmarking as well.

from virgil.

k-sareen avatar k-sareen commented on June 9, 2024

Though Virgil already has a lot of:

val = something();
if (val == null) {
    fail("val is null");
}

which would be cleaned up by the ? operator.

The major way I've seen null being used (and correct me if I'm wrong), is for initialization, which I think would benefit from the ? operator.

from virgil.

k-sareen avatar k-sareen commented on June 9, 2024

Yes that makes sense to me.

What do you imagine the semantics of a nullable type are? For the sake of argument, let's take a user defined type Foo that has a field var length: int and function def bar() -> f64. Now, is the following program valid?

var f: Foo? = null;
System.puts(f.length)
System.puts(f.bar())

In Rust, you can't access the internal type T without unpacking the Option<T> (either with a match/if statement or the ? operator), and Kotlin, for example, does kinda allow it, but will throw a compilation error. Kotlin has the "safe call operator" similar, but not the same, to Rust's ? operator, wherein any nullable type's fields or methods can be accessed by doing f?.length. I personally like the Option type in Rust more due to its explicitness (note that it's not verbose due to the existence of the ? operator), but it may just be Stockholm syndrome, haha.

Also, semantics-wise, it'd be good to have predefined functions on every nullable type such as def is_some() -> bool and def is_none() -> bool as well.

Rust optimizes Option<T> to be the same size as T for pointer types such as Box, &T, &mut T etc. (effectively null pointer optimization). This would be an important optimization imo.

While we're here, how does Virgil want to support errors/exception handling? The Rust (or sort of functional) style with the Result type (so explicit error propagation), or the Java style exception handling? A Result type is almost an Option type, so it could be beneficial to discuss this. The ? operator in Rust can be used for both checking and returning an Option and Result value immediately. That is,

fn some_function() -> Result<f64, Err> {
    let f = some_other_function()?;
    // use f
}

is equivalent to

fn some_function() -> Result<f64, Err> {
    let f = match some_other_function() {
        Ok(f) => f;
        Err(e) => return Err(e);
    };
    // use f
}

If the goal is to have explicit error propagation like with a Result type, then I think an operator for quickly returning errors like ? operator in Rust is a good idea in order to reduce repetitive, boring, boilerplate code (I'm looking at you, Go).

from virgil.

titzer avatar titzer commented on June 9, 2024

Yes that makes sense to me.

What do you imagine the semantics of a nullable type are? For the sake of argument, let's take a user defined type Foo that has a field var length: int and function def bar() -> f64. Now, is the following program valid?

var f: Foo? = null;
System.puts(f.length)
System.puts(f.bar())

I think an error for accessing fields or calling methods on a nullable type would be in order. (Otherwise, what's the point, ja?)

In Rust, you can't access the internal type T without unpacking the Option<T> (either with a match/if statement or the ? operator), and Kotlin, for example, does kinda allow it, but will throw a compilation error. Kotlin has the "safe call operator" similar, but not the same, to Rust's ? operator, wherein any nullable type's fields or methods can be accessed by doing f?.length. I personally like the Option type in Rust more due to its explicitness (note that it's not verbose due to the existence of the ? operator), but it may just be Stockholm syndrome, haha.

I think I like some shorthands for either forcing a nullcheck (with !NullPointerException) or for accessing fields/methods, similar to Kotlin.

The expression e.?f, representing a null-tolerant field load, would be sugar for {var tmp = e; if(tmp != null, tmp.f) } and similarly for method calls, e.m(exprs) is sugar for {var tmp = e; if(tmp != null, tmp(exprs) } (note, exprs not evaluated if e is null.

Also, semantics-wise, it'd be good to have predefined functions on every nullable type such as def is_some() -> bool and def is_none() -> bool as well.

Sure, you could just use T.?(e) and e == null for that, because the type T would represent the non-null type.

Rust optimizes Option<T> to be the same size as T for pointer types such as Box, &T, &mut T etc. (effectively null pointer optimization). This would be an important optimization imo.

Yeah, I want to generally upgrade the middle of the compiler to represent ADTs more efficiently, so it's effectively like using a tag bit for non-reference types and just using a null pointer for reference types.

While we're here, how does Virgil want to support errors/exception handling? The Rust (or sort of functional) style with the Result type (so explicit error propagation), or the Java style exception handling? A Result type is almost an Option type, so it could be beneficial to discuss this. The ? operator in Rust can be used for both checking and returning an Option and Result value immediately. That is,

No on using Java-style exceptions, so more in the style of encoding errors into return types (sometimes as ADTs). It turns out I sometimes end up encoding errors as configurable behavior on an object, like how DataReader has a mutable onError function member, or passing an additional argument which is an error generator/collector and returning a default value. (E.g. both the Virgil compiler and the Wizard verifier do this a lot).

fn some_function() -> Result<f64, Err> {
    let f = some_other_function()?;
    // use f
}

is equivalent to

fn some_function() -> Result<f64, Err> {
    let f = match some_other_function() {
        Ok(f) => f;
        Err(e) => return Err(e);
    };
    // use f
}

If the goal is to have explicit error propagation like with a Result type, then I think an operator for quickly returning errors like ? operator in Rust is a good idea in order to reduce repetitive, boring, boilerplate code (I'm looking at you, Go).

I can see the value of having an explicit kind of error-propagating type, so if there was a way to integrate that in a more Virgilistic way (I avoid build too many named types, particularly capitalized named types), that'd be neat.

from virgil.

k-sareen avatar k-sareen commented on June 9, 2024

I think an error for accessing fields or calling methods on a nullable type would be in order. (Otherwise, what's the point, ja?)

Haha yes of course. I meant more about the syntax and UX, I guess.

The expression e.?f, representing a null-tolerant field load, would be sugar for {var tmp = e; if(tmp != null, tmp.f) } and similarly for method calls, e.m(exprs) is sugar for {var tmp = e; if(tmp != null, tmp(exprs) } (note, exprs not evaluated if e is null.

I think ideally it's the same syntax for accessing a field and a method like how Kotlin does it.

Sure, you could just use T.?(e) and e == null for that, because the type T would represent the non-null type.

Exactly yeah. Just better to have semantic/descriptive function names in my personal subjective opinion.

I can see the value of having an explicit kind of error-propagating type, so if there was a way to integrate that in a more Virgilistic way (I avoid build too many named types, particularly capitalized named types), that'd be neat.

Ah right -- I just realized that yeah Virgil does not have built-in types that start with a capital letter other than Array. Is there any particular reason for that? Don't want to potentially have to deal with shadowing user-defined types? Or something else?

Could do something like an err type? Or result equivalently (this is probably more semantically appropriate).

from virgil.

titzer avatar titzer commented on June 9, 2024

Ah right -- I just realized that yeah Virgil does not have built-in types that start with a capital letter other than Array. Is there any particular reason for that? Don't want to potentially have to deal with shadowing user-defined types? Or something else?

Yes, I am trying to keep a very strict separation between what is a library and what is in the language, and language types looking like library types can cause some confusion. Array<T> was originally a placeholder for making a decision about a better syntax. It stuck because it's easy to read.

Could do something like an err type? Or result equivalently (this is probably more semantically appropriate).

Perhaps. One thing about errors that I have dealt with a lot recently is that the error cases basically have arguments, such as which file or line number they occurred at, etc. There is some application-level data that might be programmatically attached and useful.

from virgil.

k-sareen avatar k-sareen commented on June 9, 2024

Perhaps. One thing about errors that I have dealt with a lot recently is that the error cases basically have arguments, such as which file or line number they occurred at, etc. There is some application-level data that might be programmatically attached and useful.

Right. But these fields can be transparently inserted by the compiler at compilation time without the user having to specify them (well I don't know if it's specifically easy to do in Virgil, but it is theoretically possible at least). Unless you imagine exposing these fields (i.e. file name, line number) to the programmer? I don't immediately see how it's useful or relevant, since generally something like an enum with a field (describing the error) is good enough for the programmer to report errors.

from virgil.

k-sareen avatar k-sareen commented on June 9, 2024

We could do something like:

def fn_that_returns_error(fail: bool) -> !UserType {
    if (fail) {
        return err("some error");
    } else {   
        return UserType.new();
    }
}

or some other similar syntax for returning errors. Here !UserType is a union of err<T>(val: T) and UserType.

Though maybe this discussion should be moved to a separate GitHub issue.

from virgil.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.