5
$\begingroup$

Background

Following a set of simple rules, I can determine the output type of any statement. For example, adding two integers gives me an integer, list.get() gives me whatever type the list contains and so on.

A code block is just one statement containing any number of statements. Its output type is defined as the output type of the last statement:

// s will be a string ("20")
s = {
    a = 10
    to_string(a * 2)
}

This way, function return types are also determined just from the code written in that function (they don't need to be specified), but there are cases (more complicated functions etc) where you want to be sure you can't accidentally return a wrong type and mess up the function signature - it would be nice to specify the return type you want and know that this will be enforced.

Issues with conventional syntax

I could simply add the option to specify function return types, but I have already found situations where I wanted to annotate variables with their types, too. So instead of adding multiple syntactical ways of forcing specific types, I would prefer having only one way to specify the return type that works for every situation - for example after the statement in question:

// returns an int
x = 10
// same here
x = { 10 }
// same here, but the compiler enforces it
x = { 10 } :: int
// works for functions
fn returns_int() { 10 } :: int
x = returns_int()

This example would place the type after the function's body - for longer functions, it would likely be off-screen. So a prefix notation seems to be my best bet:

// me, currently - no type annotation
fn double(a int) { 2 * a }

// rust, using an arrow
fn double(a: int) -> int { 2 * a }
// the arrow can be "glued" to the function body rather than its signature:
// signature: fn double(a: int)
// body: -> int { 2 * a }
// => functions return type is int since its body returns int

// but outside of functions, this looks confusing to me:
a = -> int { 12 }

Actual question

If the only way to explicitly specify types was to annotate a statement/code block, which syntax would be the preferred way to do this?

Note: Obviously, this has to be optional syntax - we can't just change every 1 in code to 1 :: int or int 1.

$\endgroup$
2
  • $\begingroup$ The Swift way: { ()-> Int in 1 }. The word “in” serves to divide the type from the body. $\endgroup$
    – Bbrk24
    May 23, 2023 at 20:33
  • $\begingroup$ The conventional syntax is like let x: int = { ... }; $\endgroup$
    – kaya3
    Jun 5, 2023 at 14:21

2 Answers 2

6
$\begingroup$

Type Ascriptions

What you are asking about is called a Type Ascription.
Haskell and derivative languages expose it via what they call Expression Type-Signatures.
It looks like this: 42 :: Int, and you can use it like this: f (42 :: Int).

Further than this, it can also be used to narrow down the desired type from an expression, say to remove types from an intersection, or to perform a downcast, or to select a member from an overloaded set.
It may even be used to select how to compute an expression depending on functions polymorphic on their return type.
In Haskell, you may see code like this:

class Read a where
    read: String -> a

instance Read String where
    read = id

instance Read Int where
    read = parseInt

main =
    putStrLn (read "42" :: Int)

Here, the read "42" expression alone is ambiguous - has it type Int or String? - so the ascription expression narrows the type down to Int, the correct function is selected, and everything computes.

In Scala it's spelt x.asInstanceOf[Int].
In fact, you actually can use casts to do all of the above in C-like languages. For instance, D actually requires using a cast as a type ascription to take the address of a function that is overloaded (eg. &f may fail, so you have to explicitly say cast(T function(U, V)) &f for it to work).

This example would place the type after the function's body - for longer functions, it would likely be off-screen

C casts go first!

$\endgroup$
4
  • $\begingroup$ "D actually requires using a cast as a type ascription" Isn't this ambiguous between "just use the function that returns int" vs "use the function that returns float, but then cast that float to an int"? (btw, thanks - just knowing what this is called was very helpful for me) $\endgroup$
    – Dummi26
    Jun 6, 2023 at 14:53
  • $\begingroup$ @Dummi26 It doesn't have to be ambiguous. For example you could unify the two read functions above under an intersection type (String -> String & String -> Int), where ambiguity is automatically solved by an upcast to String -> (String | Int) - at which point the language tells you to figure out for yourself what the returned value actually is. But D is not smart enough for that kind of things, and taking the address of an overloaded function without a cast makes the compiler complain. $\endgroup$
    – Longinus
    Jun 6, 2023 at 15:06
  • $\begingroup$ @Dummi26 You can see the error here: run.dlang.io/is/F0xQBV $\endgroup$
    – Longinus
    Jun 6, 2023 at 15:10
  • $\begingroup$ @Dummi26 Wouldn't that be "use function returning int" versus "cast value to int" (not function) ? cast(void function(int)) versus cast(int). Of course, there's still the problem of select (valid) versus cast/force: cast(float function(string) &f. $\endgroup$
    – Pablo H
    Sep 6, 2023 at 13:31
5
$\begingroup$

Does the Rust style of permitting type annotations on the binding itself seem amenable to you?

s: string = {
    a: int = 10
    to_string(a * 2)
}

x: int = 10

x: int = { 10 }

// You could use this with function literals like so, if you wanted
double: fn(int) -> int = ...

a: int = { 12 }

In my mind, this is not really about the "return type" of the expression, but just asserting or ascribing the type of the expression itself.

One big difference between this and your own proposal is that an expression level form, like x :: type, allows you to ascribe types to subexpressions in a more complex composite expression, e.g. foo(bar(x, y :: int), z), whereas annotations on bindings would require you to decompose that expression if you wanted to ascribe a type to an inner piece.

$\endgroup$
1
  • 1
    $\begingroup$ From what you write, it seems ascribing to expressions is more flexible/powerful than ascribing to bindings. What's the tradeoff? Are there benefits to ascribing to bindings and/or demerits to ascribing to expressions? $\endgroup$
    – Pablo H
    Sep 6, 2023 at 13:38

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .