Iconsistent type alias compatibility behavior.
This is allowed:
type alias1 = int
type alias2 = int
fn main() {
x := alias1(5)
x = alias2(6)
printf("%d\n", x)
}
This is not allowed:
type alias1 = int
type alias2 = int
fn main() {
x := []alias1{5}
x = []alias2{6}
printf("%v\n", x)
}
This is related to #340, but I think it's a different issue because the former needs to include the module name in the type names.
Technically, this behavior is correct:
-
alias1andalias2are not equivalent (as they have different identifiers), but compatible (both are integer types) -
[]alias1and[]alias2are neither equivalent, nor compatible (as they have inequivalent base types)
I think, Rule 1 should remain in effect, as I don't want to require as many explicit casts as Go does.
Should we relax Rule 2 to make []alias1 and []alias2 compatible? It would be more consistent with Rule 1. However, I see two problems here:
A. Compatibility is a property that is just checked, rather than enforced by some implicit casts. Casting a dynamic array at runtime is a highly nontrivial procedure. I thought it should be visible to the user, so I only allowed it as an explicit cast.
B. Should [3]int8 and [3]uint16 also be compatible? What about []real and []real32? (Notice that int8 and uint16 have the same 64-bit internal representation as intermediate values, while [3]int8 and [3]uint16 don't.)
IMO we shouldn't have []int8 and []uint16 et al compatible, the reason is that []alias1 and []alias2 are actually trivially compatible, since they're technically the same type. Do we need this non-equivalence in type assignments? Feels inconsistent to me.
[]alias1 and []alias2 are actually trivially compatible, since they're technically the same type.
What you mean here is that []alias1 and []alias2 are equivalent. Then alias1 and alias2 should also be equivalent. Okay.
type Rect = struct {a, b: real}
type Ellipse = struct {a, b: real}
fn area(r: Rect): real {return r.a * r.b}
Are Rect and Ellipse equivalent? If so, can I pass an Ellipse to area()? If not, what's the difference between Rect/Ellipse and []alias1/[]alias2?
I think they're not the same, since they're not the same actual base type. So there's 2 real options in my opinion:
- Type assignment makes a base type.
- Type assignment makes an alias type.
Base types are not equivalent to each other, alias types are equivalent as long as their base type is equivalent.
Having type assignments be base types will break a lot of things, as we discussed in #337. Having them be compatible types seems too complicated in my opinion, so more logical thing for them is to be aliases.
@skejeton And thus you end up with a contradiction.
Having type assignments be base types will break a lot of things... so more logical thing for them is to be aliases.
[]alias1 and []alias2 are actually trivially compatible, since they're technically the same type.
So []int and []int are equivalent.
Are Rect and Ellipse equivalent? — I think they're not the same, since they're not the same actual base type.
So struct {a, b: real} and struct {a, b: real} are not equivalent.
How is that possible? What's the difference between []int/[]int and struct/struct?
P.S. I assume that you're using the term "base type" in a meaning different from the Umka reference. For you, it means a "distinct type" or "something that is not an alias", rather than a base type of a pointer or an array.
To outline the concepts:
- Base type - A distinct type, created using
structor pre-defined (str, int, map, []type) etc - Alias type - A pointer to a base type, alias types can be equivalent if both base types are are the same type.
Rect and Ellipse examples create distinct base types because they're created with struct even though they have an identical layout.
The algorithm for type compatibility:
base_cmp(base_type_a, base_type_b) = base_type_a == base_type_b
Base types are not compatible
alias_cmp(type_a, type_b) = base_cmp(get_base_type(type_a), get_base_type(type_b))
The comparison is the same, just before checking type compatibility, alias types should "unwrap" the base types that they point to, and check if they're the same type.
@skejeton Are []alias1 and []alias2 different base types?
@skejeton Are
[]alias1and[]alias2different base types?
No: get_base_type([]alias1) = []int and get_base_type([]alias2) = []int hence []alias1 and []alias2 are the same base type.
@skejeton Both struct and [] are the ways of constructing new types from existing ones. However, you seem to insist that a different rule be applied to struct than to any other type constructors, so that
-
[]intand[]intare the same (and thus equivalent and compatible) -
struct {a, b: real}andstruct {a, b: real}are not the same (and thus neither equivalent nor compatible)
Do I understand your vision correctly?
Pretty much yes, I see []t and map[tt]t compatible with []t and map[tt]t respectively, where t and tt is the same. To illustrate it:
x := []int{}
y := []int{}
x = y // OK
x := map[str]int{}
y := map[str]int{}
x = y // OK
type A = struct {x: int}
type B = struct {x: int}
x := A{1}
y := B{32}
x = y // ERROR
@skejeton Now I understand you. Which doesn't mean I share the same view.
- You essentially introduce a new (and not very easy to grasp) concept to the language, i.e., a classification of type constructors by whether they produce distinct types or the same type:
- Distinct:
struct,interface(?),fn(?),enum(?) - Same:
[],map[T],[N](?)
- You introduce new inconsistencies:
type VertexIndex = int
type Edge = [2]VertexIndex
type CanvasSize = [2]int // pixels
fn setCanvasSize(size: CanvasSize) {/*...*/}
var edge: Edge
setCanvasSize(edge) // OK
fn setPoint(point: struct {x, y: real}) {/*...*/}
var point: struct {x, y: real}
setPoint(point) // Error: Incompatible types struct {x, y: real} and struct {x, y: real}
Wait, how does type work right now?
@skejeton In general, Umka uses structural type equivalence, i.e., two types are equivalent iff they have the same layout, field/parameter names, etc. So the type name doesn't really matter.
However, there are two exceptions where Umka relies on nominal type equivalence instead:
- Two types are not equivalent if they both have names and these names are different
- A type used as a method receiver type must have a name
Two types are not equivalent if they both have names and these names are different
If alias1 and alias2 aren't equivalent, how does the first example work (when they're both non arrays)
My take on this situation:
There are two relations types can have:
- compatibility - the type can be explicitly casted to the other
- equivalency - the type can be implicitly casted to the other
Equivalent:
- same types
- a "base" type and an alias
- some numerical types (I. e. promoting int32 to int, not the other way around)
Compatible:
- some numerical types. Those where an error could occur, or casting between ints and reals
- structs with the same fields
- different aliases based on the same type
- interfaces
Arrays and maps will just inherit the properties of their base types.
If alias1 and alias2 aren't equivalent, how does the first example work (when they're both non arrays)
Because all integer types are compatible even if they are not equivalent (int8 and uint16 are also compatible)
I see it now. I think I agree with Marek's opinion in here, the only thing I'm not entirely sure about is whether having different aliases based on the same type be compatible or equivalent. Choosing this would change whether in the original example we need to explicitly cast alias1 to alias2 or not.
@marekmaskarinec I need to think more about it, but here is what seems doubtful to me for now:
- Equivalence becomes non-commutative:
T == UbutU != T. This is not what people generally expect when they say "equivalent" - You cannot pass
0tofn (x: real)because0isint - You can add an
int32toth.uu, but notth.iutoth.uu
@ske2004 If we accept that:
- Our resolution of #520 and your comment https://github.com/vtereshkov/umka-lang/issues/520#issuecomment-2932501833 are valid
- Dynamic arrays and structures should not be treated differently
- All integer types should remain compatible, up to overflow,
then we should close this issue without any further actions taken.
Dynamic arrays and structures should not be treated differently I don't understand because right now you can do the following:
type A = []int
type B = []int
fn main() {
x := A{}
y := B(x)
}
but not this:
type A = struct{}
type B = struct{}
fn main() {
x := A{}
y := B(x)
}
But this is exactly how I expect it to work.
and as you said, integers (and reals, apprently), can work without explicit casts:
type A = int
type B = uint
fn main() {
var x: A
y := B(x)
}
In this case, I think the issue can be closed!
@ske2004
Dynamic arrays and structures should not be treated differently... I don't understand because right now you can do the following...
Hmm, you're right: dynamic arrays are treated differently, due to covariant array casts. But should they? I now doubt if your first example should be valid. A = []int and B = []int are both named types that may be very different semantically, have different method sets, etc.
But OK, let's keep it valid. At least, covariant array casts are always safe, as they are element-wise and thus expect the array base types to be explicitly castable. So this is still invalid:
type (
T = struct{_: ^void}
U = struct{_: ^void}
A = []T
B = []U
)
fn main() {
x := A{}
y := B(x) // Cannot cast T to U
}
In this case, I think the issue can be closed!
Well, I'm closing this, even though the behavior of your two original examples in this issue hasn't changed. A declaration like type X = Y declares a new, inequivalent type, possibly with a different method set, rather than an alias. To workaround this, you still need an explicit cast. Our resolution for #520 doesn't render such types equivalent, it only deals with explicit casts. The same for covariant array casts, which are also explicit.
Two types are equivalent if ... They are declared types that have the same identifier. Any other declared types are not equivalent
If a value s of type S is given where a value t of some other type T is expected, the s can be explicitly converted (cast) to t if ... S and T are the same type except the type name, i.e., they are declared as type S = T or type T = S ... S is []U, T is []V and U can be explicitly converted to V
@ske2004 To be honest, I'm still not satisfied with the explicit casts between unrelated named types, which are allowed due to the covariant array rules:
type (
Id = int
Ids = []Id
Val = real
Vals = []Val
Map = map[Id]Val
)
fn (vs: ^Vals) mean(): Val {
s := 0.0
for _, v in vs {
s += v
}
return s / len(vs^)
}
fn main() {
m := Map{13: 9.81, 42: 9.79, 666: 9.84}
ids := Ids(keys(m))
gravity := Vals(ids).mean()
printf("%v\n", gravity) // 240.333 - WTF?!
}
But, if we forbid them, we should also forbid casting Id to Val, which I'm not willing to do.