Implement nullable shorthand for params
This commit is contained in:
parent
7e538ed8aa
commit
9db5b12c31
138
docs/tutorial.md
138
docs/tutorial.md
@ -224,6 +224,9 @@ This also prevents chain assignments so use parentheses:
|
|||||||
|
|
||||||
## Nullability
|
## Nullability
|
||||||
|
|
||||||
|
Nullability is part of the type. `String` is non-null, `String?` is nullable. Use `!!` to assert non-null and throw
|
||||||
|
`NullReferenceException` if the value is `null`.
|
||||||
|
|
||||||
When the value is `null`, it might throws `NullReferenceException`, the name is somewhat a tradition. To avoid it
|
When the value is `null`, it might throws `NullReferenceException`, the name is somewhat a tradition. To avoid it
|
||||||
one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null,
|
one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null,
|
||||||
the operation won't be performed and the result will be null. Here is the difference:
|
the operation won't be performed and the result will be null. Here is the difference:
|
||||||
@ -242,6 +245,9 @@ the operation won't be performed and the result will be null. Here is the differ
|
|||||||
assert( ref?() == null )
|
assert( ref?() == null )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
Note: `?.` is still a typed operation. The receiver must have a compile-time type that declares the member; if the
|
||||||
|
receiver is `Object`, cast it first or declare a more specific type.
|
||||||
|
|
||||||
There is also "elvis operator", null-coalesce infix operator '?:' that returns rvalue if lvalue is `null`:
|
There is also "elvis operator", null-coalesce infix operator '?:' that returns rvalue if lvalue is `null`:
|
||||||
|
|
||||||
null ?: "nothing"
|
null ?: "nothing"
|
||||||
@ -425,8 +431,6 @@ Almost the same, using `val`:
|
|||||||
val foo = 1
|
val foo = 1
|
||||||
foo += 1 // this will throw exception
|
foo += 1 // this will throw exception
|
||||||
|
|
||||||
# Constants
|
|
||||||
|
|
||||||
Same as in kotlin:
|
Same as in kotlin:
|
||||||
|
|
||||||
val HalfPi = π / 2
|
val HalfPi = π / 2
|
||||||
@ -434,6 +438,133 @@ Same as in kotlin:
|
|||||||
Note using greek characters in identifiers! All letters allowed, but remember who might try to read your script, most
|
Note using greek characters in identifiers! All letters allowed, but remember who might try to read your script, most
|
||||||
likely will know some English, the rest is the pure uncertainty.
|
likely will know some English, the rest is the pure uncertainty.
|
||||||
|
|
||||||
|
# Types and inference
|
||||||
|
|
||||||
|
Lyng uses Kotlin-style static types with inference. You can always write explicit types, but in most places the compiler
|
||||||
|
can infer them from literals, defaults, and flow analysis.
|
||||||
|
|
||||||
|
## Type annotations
|
||||||
|
|
||||||
|
Use `:` to specify a type:
|
||||||
|
|
||||||
|
var x: Int = 10
|
||||||
|
val label: String = "count"
|
||||||
|
fun clamp(x: Int, min: Int, max: Int): Int { ... }
|
||||||
|
|
||||||
|
`Object` is the top type. If you omit a type and there is no default value, the parameter is `Object` by default:
|
||||||
|
|
||||||
|
fun show(x) { println(x) } // x is Object
|
||||||
|
|
||||||
|
For nullable types, add `?`:
|
||||||
|
|
||||||
|
fun showMaybe(x: Object?) { ... }
|
||||||
|
fun parseInt(s: String?): Int? { ... }
|
||||||
|
|
||||||
|
There is also a nullable shorthand for untyped parameters and constructor args: `x?` means `x: Object?`.
|
||||||
|
It cannot be combined with an explicit type annotation.
|
||||||
|
|
||||||
|
class A(x?) { ... } // x: Object?
|
||||||
|
fun f(x?) { x == null } // x: Object?
|
||||||
|
|
||||||
|
`void` is a singleton value of the class `Void`. `Void` can be used as an explicit return type:
|
||||||
|
|
||||||
|
fun log(msg): Void { println(msg); void }
|
||||||
|
|
||||||
|
`Null` is the class of `null`. It is a singleton type and mostly useful for type inference results.
|
||||||
|
|
||||||
|
## Type inference
|
||||||
|
|
||||||
|
The compiler infers types from:
|
||||||
|
|
||||||
|
- literals: `1` is `Int`, `1.0` is `Real`, `"s"` is `String`, `'c'` is `Char`
|
||||||
|
- defaults: `fun f(x=1, name="n")` infers `x: Int`, `name: String`
|
||||||
|
- assignments: `val x = call()` uses the return type of `call`
|
||||||
|
- returns and branches: the result type of a block is the last expression, and if any branch is nullable,
|
||||||
|
the inferred type becomes nullable
|
||||||
|
- numeric ops: `Int` and `Real` stay `Int` when both sides are `Int`, and promote to `Real` on mixed arithmetic
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
fun inc(x=0) = x + 1 // (Int)->Int
|
||||||
|
fun maybe(flag) { if(flag) 1 else null } // ()->Int?
|
||||||
|
|
||||||
|
Untyped locals are allowed, but their type is fixed on the first assignment:
|
||||||
|
|
||||||
|
var x
|
||||||
|
x = 1 // x becomes Int
|
||||||
|
x = "one" // compile-time error
|
||||||
|
|
||||||
|
var y = null // y is Object?
|
||||||
|
val z = null // z is Null
|
||||||
|
|
||||||
|
Empty list/map literals default to `List<Object>` and `Map<Object,Object>` until a more specific type is known:
|
||||||
|
|
||||||
|
val xs = [] // List<Object>
|
||||||
|
val ys: List<Int> = [] // List<Int>
|
||||||
|
|
||||||
|
## Flow analysis
|
||||||
|
|
||||||
|
Lyng uses flow analysis to narrow types inside branches:
|
||||||
|
|
||||||
|
fun len(x: String?): Int {
|
||||||
|
if( x == null ) return 0
|
||||||
|
// x is String (non-null) in this branch
|
||||||
|
return x.length
|
||||||
|
}
|
||||||
|
|
||||||
|
`is` checks and `when` branches also narrow types:
|
||||||
|
|
||||||
|
fun kind(x: Object) {
|
||||||
|
if( x is Int ) return "int"
|
||||||
|
if( x is String ) return "string"
|
||||||
|
return "other"
|
||||||
|
}
|
||||||
|
|
||||||
|
Narrowing is local to the branch; after the branch, the original type is restored.
|
||||||
|
|
||||||
|
## Casts and unknown types
|
||||||
|
|
||||||
|
Use `as` for explicit casts. The compiler inserts casts only when it can be valid and necessary. If a cast fails at
|
||||||
|
runtime, it throws `ClassCastException`. If the value is nullable, `as T` implies a non-null assertion.
|
||||||
|
|
||||||
|
Member access is resolved at compile time. Only members of `Object` are available on unknown types; non-Object members
|
||||||
|
require an explicit cast:
|
||||||
|
|
||||||
|
fun f(x) { // x is Object
|
||||||
|
x.toString() // ok (Object member)
|
||||||
|
x.size() // compile-time error
|
||||||
|
(x as List).size() // ok
|
||||||
|
}
|
||||||
|
|
||||||
|
This avoids runtime name-resolution fallbacks; all symbols must be known at compile time.
|
||||||
|
|
||||||
|
## Generics and bounds
|
||||||
|
|
||||||
|
Generic parameters are declared with `<...>`:
|
||||||
|
|
||||||
|
fun id<T>(x: T): T = x
|
||||||
|
class Box<T>(val value: T)
|
||||||
|
|
||||||
|
Bounds use `:` and can combine with `&` (intersection) and `|` (union):
|
||||||
|
|
||||||
|
fun sum<T: Int | Real>(x: T, y: T) = x + y
|
||||||
|
class Named<T: Iterable & Comparable>(val data: T)
|
||||||
|
|
||||||
|
Type arguments are usually inferred from call sites:
|
||||||
|
|
||||||
|
val b = Box(10) // Box<Int>
|
||||||
|
val s = id("ok") // T is String
|
||||||
|
|
||||||
|
## Variance
|
||||||
|
|
||||||
|
Generic types are invariant by default, so `List<Int>` is not a `List<Object>`.
|
||||||
|
Use declaration-site variance when you need it:
|
||||||
|
|
||||||
|
class Source<out T>(val value: T)
|
||||||
|
class Sink<in T> { fun accept(x: T) { ... } }
|
||||||
|
|
||||||
|
`out` makes the type covariant (only produced), `in` makes it contravariant (only consumed).
|
||||||
|
|
||||||
# Defining functions
|
# Defining functions
|
||||||
|
|
||||||
fun check(amount) {
|
fun check(amount) {
|
||||||
@ -1330,7 +1461,7 @@ than enum arrays, until `Lynon.encodeTyped` will be implemented.
|
|||||||
var result = null // here we will store the result
|
var result = null // here we will store the result
|
||||||
>>> null
|
>>> null
|
||||||
|
|
||||||
# Integral data types
|
# Built-in types
|
||||||
|
|
||||||
| type | description | literal samples |
|
| type | description | literal samples |
|
||||||
|--------|---------------------------------|---------------------|
|
|--------|---------------------------------|---------------------|
|
||||||
@ -1340,6 +1471,7 @@ than enum arrays, until `Lynon.encodeTyped` will be implemented.
|
|||||||
| Char | single unicode character | `'S'`, `'\n'` |
|
| Char | single unicode character | `'S'`, `'\n'` |
|
||||||
| String | unicode string, no limits | "hello" (see below) |
|
| String | unicode string, no limits | "hello" (see below) |
|
||||||
| List | mutable list | [1, "two", 3] |
|
| List | mutable list | [1, "two", 3] |
|
||||||
|
| Object | top type for all values | |
|
||||||
| Void | no value could exist, singleton | void |
|
| Void | no value could exist, singleton | void |
|
||||||
| Null | missing value, singleton | null |
|
| Null | missing value, singleton | null |
|
||||||
| Fn | callable type | |
|
| Fn | callable type | |
|
||||||
|
|||||||
@ -2641,8 +2641,31 @@ class Compiler(
|
|||||||
access
|
access
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nullable shorthand: "x?" means Object? (or makes explicit type nullable)
|
||||||
|
var nullableHint = false
|
||||||
|
val nullableHintPos = cc.savePos()
|
||||||
|
cc.skipWsTokens()
|
||||||
|
if (cc.peekNextNonWhitespace().type == Token.Type.QUESTION) {
|
||||||
|
cc.nextNonWhitespace()
|
||||||
|
nullableHint = true
|
||||||
|
} else {
|
||||||
|
cc.restorePos(nullableHintPos)
|
||||||
|
}
|
||||||
|
|
||||||
// type information (semantic + mini syntax)
|
// type information (semantic + mini syntax)
|
||||||
val (typeInfo, miniType) = parseTypeDeclarationWithMini()
|
if (nullableHint) {
|
||||||
|
val afterHint = cc.savePos()
|
||||||
|
cc.skipWsTokens()
|
||||||
|
if (cc.peekNextNonWhitespace().type == Token.Type.COLON) {
|
||||||
|
throw ScriptError(t.pos, "nullable shorthand '?' cannot be combined with explicit type")
|
||||||
|
}
|
||||||
|
cc.restorePos(afterHint)
|
||||||
|
}
|
||||||
|
var (typeInfo, miniType) = parseTypeDeclarationWithMini()
|
||||||
|
if (nullableHint) {
|
||||||
|
typeInfo = makeTypeDeclNullable(typeInfo)
|
||||||
|
miniType = miniType?.let { makeMiniTypeNullable(it) }
|
||||||
|
}
|
||||||
|
|
||||||
var defaultValue: Statement? = null
|
var defaultValue: Statement? = null
|
||||||
cc.ifNextIs(Token.Type.ASSIGN) {
|
cc.ifNextIs(Token.Type.ASSIGN) {
|
||||||
@ -2713,6 +2736,31 @@ class Compiler(
|
|||||||
return parseTypeUnionWithMini()
|
return parseTypeUnionWithMini()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun makeTypeDeclNullable(type: TypeDecl): TypeDecl {
|
||||||
|
if (type.isNullable) return type
|
||||||
|
return when (type) {
|
||||||
|
TypeDecl.TypeAny -> TypeDecl.TypeNullableAny
|
||||||
|
TypeDecl.TypeNullableAny -> type
|
||||||
|
is TypeDecl.Function -> type.copy(nullable = true)
|
||||||
|
is TypeDecl.TypeVar -> type.copy(nullable = true)
|
||||||
|
is TypeDecl.Union -> type.copy(nullable = true)
|
||||||
|
is TypeDecl.Intersection -> type.copy(nullable = true)
|
||||||
|
is TypeDecl.Simple -> TypeDecl.Simple(type.name, true)
|
||||||
|
is TypeDecl.Generic -> TypeDecl.Generic(type.name, type.args, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeMiniTypeNullable(type: MiniTypeRef): MiniTypeRef {
|
||||||
|
return when (type) {
|
||||||
|
is MiniTypeName -> type.copy(nullable = true)
|
||||||
|
is MiniGenericType -> type.copy(nullable = true)
|
||||||
|
is MiniFunctionType -> type.copy(nullable = true)
|
||||||
|
is MiniTypeVar -> type.copy(nullable = true)
|
||||||
|
is MiniTypeUnion -> type.copy(nullable = true)
|
||||||
|
is MiniTypeIntersection -> type.copy(nullable = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseTypeUnionWithMini(): Pair<TypeDecl, MiniTypeRef> {
|
private fun parseTypeUnionWithMini(): Pair<TypeDecl, MiniTypeRef> {
|
||||||
var left = parseTypeIntersectionWithMini()
|
var left = parseTypeIntersectionWithMini()
|
||||||
val options = mutableListOf(left)
|
val options = mutableListOf(left)
|
||||||
|
|||||||
@ -146,4 +146,24 @@ class TypesTest {
|
|||||||
println("Result: "+limit)
|
println("Result: "+limit)
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNullableHints() = runTest {
|
||||||
|
eval("""
|
||||||
|
// nullable, without type os Object?
|
||||||
|
class N(x=null)
|
||||||
|
assertEquals(null, N().x)
|
||||||
|
assertEquals("foo", N("foo").x)
|
||||||
|
// nullable shortcut (x?)is same as (var x: Object?)
|
||||||
|
class A(x?)
|
||||||
|
assertEquals(null, A(null).x)
|
||||||
|
assertEquals("ok", A("ok").x)
|
||||||
|
|
||||||
|
// same in function: x? is a shortcut for (x: Object?)
|
||||||
|
fun f(x?) = x?.let { x + "!" }
|
||||||
|
assertEquals(null, f(null))
|
||||||
|
assertEquals("ok!", f("ok"))
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user