(Following up on the discussion in #3864.)
It would be useful to have a version of alias that creates type aliases that cannot be substituted for one another. For example (calling this new feature typedef for the sake of discussion), with this feature we'd get a compiler error for a type mismatch for the following:
typedef meters = Int32
typedef feet = Int32
def do_something_assuming_meters(measurement : meters)
# code that does something that would go badly in the wrong units
end
x : feet = 10
do_something_assuming_meters(x)
since while x is an Int32, it's considered to be of type feet here, which would be incompatible with meters.
This is useful for having the type checker help you avoid errors in a lot of different applications, like attaching units to numbers as shown here, or my original usecase in #3864, which was distinguishing between protein and DNA sequences represented as strings.
I don't think @asterite is particularly keen on this feature, but I quite like it. I think it provides some extra type safety essentially for free. I think that type is a good name for it, as it's defining a new type.
I don't know if this is a good idea... so how this works?
typedef Feet = Int32
x : Feet = 10 # here should raise an error that 10 is not a Feet, it's Int32
We have this in lib because we can ensure these value can only be use between the library's functions, right? (like HANDLE in Windows API, we should not modify or try to understand what the real value is, but it is meaningful in Window API world)
I think it can be done by macro if you really need this.
macro type_new(new_type, from old_type)
struct {{new_type}}
def initialize(@%x : {{old_type}})
end
macro method_missing(call)
(@%x.\{{call}}).try do |%y|
if %y.is_a? {{old_type}}
{{new_type}}.new %y
else
%y
end
end
end
end
end
type_new Foo, from: Int32
type_new Bar, from: Int32
foo = Foo.new 0
bar = Bar.new 0
def only_accept_foo(x : Foo)
end
only_accept_foo foo
only_accept_foo bar # => no overload matches 'only_accept_foo' with type Bar
I'd actually like to remove type from the language :-)
Before explaining why, I want to say that this is already possible by using structs, or simply record. For example:
record Feet, value : Int32
record Meters, value : Int32
feet = Feet.new(10)
meters = Meters.new(20)
def do_something_assuming_meters(measurements : Meters)
end
do_something_assuming_meters(meters) # OK
do_something_assuming_meters(feet) # Error
LLVM will optimize this to simply use Int32, so it's as efficient as using Int32, but more type-safe.
The problem with type is this. Let's assume this:
type Feet = Int32
First, how do you construct such type? Doing:
feet : Feet = 10
is not a valid option, because the language right now doesn't have implicit type casting, and using type declarations like the above is not common at all.
Maybe we can use this syntax:
feet = Feet.new(10)
which is already closer to my record solution above.
Next, what's the type of this:
feet + 1
Or this:
feet + feet
if we have type Feet = Int32, where there's Int32+#(Int32) : Int32, how does the compiler know that the + method now suddenly needs to return Int32? Maybe we can use the rule "if a method on a type returns the underlying type, automatically cast it to the wrapping type". But for example Int32 has a sign method that returns -1, 0 or 1 according to the number's sign. So feet.sign would now return Feet, which is definitely something you don't want to happen.
The part feet + feet is trickier: is it always valid to pass a Feet value where an argument expects Int32? For example there's Number#significant(digits, base = 10) method, can I pass Feet as arguments? (that method doesn't even have type restrictions)
What's the solution to the above? Explicitly defining methods on the Feet type. We can define the + method like this:
record Feet, value : Int32 do
# Maybe we'd like to just add other feet, never integers
def +(other : Feet)
Feet.new(value + other.value)
end
end
Note that I wouldn't even consider using method_missing because it has the same problem I described above.
So 馃憥 from me on adding a type concept outside libs (I'd actually like to remove type from libs too and just use alias, chances of passing a wrong Void* pointer type are very low, and if you do that you'll immediately gets an error, so type adds very little type safety and has already the problems I described above)
Great explanation. No type outside of lib, then. I think I still like type over a simple alias, but I agree it's more a comfort feeling than actual safety.
Good points! For your examples like feet + feet, or feet = 10 I was envisioning you would have to explicitly cast with something like feet.as(Int32) + feet.as(Int32), or feet = 10.as(Feet) (which I apparently left out of my example's initialization).
But as you say, that's just the same as the record solution; I'm completely satisfied with using records for this and happy to have the feature request closed!
Great! Closing this issue then.
Most helpful comment
I'd actually like to remove
typefrom the language :-)Before explaining why, I want to say that this is already possible by using structs, or simply
record. For example:LLVM will optimize this to simply use
Int32, so it's as efficient as usingInt32, but more type-safe.The problem with
typeis this. Let's assume this:First, how do you construct such type? Doing:
is not a valid option, because the language right now doesn't have implicit type casting, and using type declarations like the above is not common at all.
Maybe we can use this syntax:
which is already closer to my
recordsolution above.Next, what's the type of this:
Or this:
if we have
type Feet = Int32, where there'sInt32+#(Int32) : Int32, how does the compiler know that the+method now suddenly needs to returnInt32? Maybe we can use the rule "if a method on atypereturns the underlying type, automatically cast it to the wrapping type". But for exampleInt32has asignmethod that returns -1, 0 or 1 according to the number's sign. Sofeet.signwould now returnFeet, which is definitely something you don't want to happen.The part
feet + feetis trickier: is it always valid to pass aFeetvalue where an argument expectsInt32? For example there'sNumber#significant(digits, base = 10)method, can I passFeetas arguments? (that method doesn't even have type restrictions)What's the solution to the above? Explicitly defining methods on the
Feettype. We can define the+method like this:Note that I wouldn't even consider using
method_missingbecause it has the same problem I described above.So 馃憥 from me on adding a
typeconcept outside libs (I'd actually like to removetypefrom libs too and just usealias, chances of passing a wrong Void* pointer type are very low, and if you do that you'll immediately gets an error, sotypeadds very little type safety and has already the problems I described above)