Implement nullable shorthand for params
This commit is contained in:
parent
7e538ed8aa
commit
9db5b12c31
140
docs/tutorial.md
140
docs/tutorial.md
@ -224,6 +224,9 @@ This also prevents chain assignments so use parentheses:
|
||||
|
||||
## 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
|
||||
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:
|
||||
@ -242,6 +245,9 @@ the operation won't be performed and the result will be null. Here is the differ
|
||||
assert( ref?() == null )
|
||||
>>> 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`:
|
||||
|
||||
null ?: "nothing"
|
||||
@ -425,8 +431,6 @@ Almost the same, using `val`:
|
||||
val foo = 1
|
||||
foo += 1 // this will throw exception
|
||||
|
||||
# Constants
|
||||
|
||||
Same as in kotlin:
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
>>> null
|
||||
|
||||
# Integral data types
|
||||
# Built-in types
|
||||
|
||||
| type | description | literal samples |
|
||||
|--------|---------------------------------|---------------------|
|
||||
@ -1340,6 +1471,7 @@ than enum arrays, until `Lynon.encodeTyped` will be implemented.
|
||||
| Char | single unicode character | `'S'`, `'\n'` |
|
||||
| String | unicode string, no limits | "hello" (see below) |
|
||||
| List | mutable list | [1, "two", 3] |
|
||||
| Object | top type for all values | |
|
||||
| Void | no value could exist, singleton | void |
|
||||
| Null | missing value, singleton | null |
|
||||
| Fn | callable type | |
|
||||
@ -1731,4 +1863,4 @@ Example with custom accessors:
|
||||
|
||||
Extension members are **scope-isolated**: they are visible only in the scope where they are defined and its children. This prevents name collisions and improves security.
|
||||
|
||||
To get details on OOP in Lyng, see [OOP notes](OOP.md).
|
||||
To get details on OOP in Lyng, see [OOP notes](OOP.md).
|
||||
|
||||
@ -2641,8 +2641,31 @@ class Compiler(
|
||||
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)
|
||||
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
|
||||
cc.ifNextIs(Token.Type.ASSIGN) {
|
||||
@ -2713,6 +2736,31 @@ class Compiler(
|
||||
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> {
|
||||
var left = parseTypeIntersectionWithMini()
|
||||
val options = mutableListOf(left)
|
||||
|
||||
@ -146,4 +146,24 @@ class TypesTest {
|
||||
println("Result: "+limit)
|
||||
""".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