From: Brendan Hansen Date: Tue, 16 Nov 2021 01:48:37 +0000 (-0600) Subject: small updates X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=3f0412c9400a6b57d894bbe41f254a7b82e51ca9;p=onyx.git small updates --- diff --git a/examples/22_interfaces.onyx b/examples/22_interfaces.onyx new file mode 100644 index 00000000..da8de3a9 --- /dev/null +++ b/examples/22_interfaces.onyx @@ -0,0 +1,132 @@ +// You may have come across the term "interfaces" in other languages such as +// Java or C#. Onyx uses the same work for a _similar_ but different concept. +// The goal of an interface is to spell out what properties a type must have +// in some generic code. In Java, this is accomplished by defining what methods +// the class must have and what their signatures must be. This is a very OOP +// approach to type constraints and does not fit well with the paradigm Onyx +// bring to the table. +// +// As the other examples make clear, Onyx achieves type generic code through +// polymorphic procedures and structures, overloaded procedures and operator +// overloading. Sometimes you want to have a type generic function, but also +// want to constrain which types are valid, due to the operations performed +// in the procedure. To make this kind of constraint more explicit, Onyx +// offers interfaces and the "where" clause. + +add_example :: () { + // Interfaces take a set of type parameters, and a set of expressions that + // must be valid in order for the constraint to be true for a set of type + // arguments. This is best explained through an example. This is a simple + // interface that takes one type parameter T, and says the expression T + T + // must be valid. This may look a little weird as T should be a type_expr, + // so you should never be able to add them. However, in the interface body, + // T actually means a value of type T. This is a little confusing at first, + // but after programming with it for a while, I think it is the right + // syntax. + + CanAdd :: interface (T: type_expr) { + T + T; + } + + // To use this interface, you must write a "where" clause for a procedure or + // polymorphic struct. Here is a simple polymorphic procedure with a where + // clause that specifies that T must fulfill the CanAdd interface. This is + // obviously a little silly, as it is pretty obvious. However, in more + // complicated code, something like this may not be obvious if you are + // calling add with a custom type. You would just get a random error message + // deep in the add procedure saying that you can not add your type. To + // make these errors a little nicer, a type constraint can be used. + add :: (x: $T, y: T) -> T where CanAdd(T) { + return x + y; + } + + // From the end-user point of view, there is nothing different about calling + // a procedure with a type constraint (where clause). + x, y := 3, 4; + z := add(x, y); + + printf("add({}, {}) = {}\n", x, y, z); +} + + +// These have to be defined out here because #operator and #match are only allowed +// as top level expressions currently. +NumberLike :: interface (T: type_expr) { + // The constraints here are a little stricter than before, as you must be + // able to cast the result of these operations back to the original type. + // This effectively means that T + T must be the same type as (or a compatible + // type of) T. + cast(typeof T) (T + T); + cast(typeof T) (T - T); + cast(typeof T) (T * T); +} + +// Here, Vector2 has the type constraint of NumberLike for T. This constraint +// is checked when Vector2 is constructed with any parameters. +Vector2 :: struct (T: type_expr) where NumberLike(T) { + x := __zero_value(T); + y := __zero_value(T); +} + +#operator + (x, y: Vector2($T)) => Vector2(T).{ x.x + y.x, x.y + y.y }; +#operator - (x, y: Vector2($T)) => Vector2(T).{ x.x - y.x, x.y - y.y }; +#operator * (x, y: Vector2($T)) => x.x * y.x + x.y * y.y; + +struct_example :: () { + // This will work fine because a value of type f32 will satisfy all the + // constraints listed in NumberLike. + v1 := Vector2(f32).{}; + println(v1); + + // This will generate an error because the dot product definition of + // multiplication of two Vector2(f32) results in a f32, which cannot + // be casted to a Vector2(f32). + v2 := Vector2(Vector2(f32)).{}; + // println(v2); +} + + +overloaded_procedure_example :: () { + // So far, all of the examples have shown how interfaces can be used to + // provide a sanity check when programming with polymorphic procedure + // and structures. Interfaces are slightly more powerful than that however + // when the are combined with overloaded procedures. + + // Take this example. Here is the same CanAdd interface from before. + CanAdd :: interface (T: type_expr) { + T + T; + } + + // This overloaded procedure has two procedures that have almost identical + // types. However the first procedure has the CanAdd type constraint on + // T. This means is in order to match the first procedure, x and y must be + // addable. If there are not, then the first procedure is not matched and + // it tries to the second procedure. Using this pattern, you can create + // compile time conditional code that introspects what values of a certain + // type can do to determine what procedure should be used. + add_if_possible :: #match { + (x: $T, y: T) -> T where CanAdd(T) { + return x + y; + }, + + (x: $T, y: T) -> T { + println("It is not possible to add x and y. Returning x."); + return x; + }, + } + + add_if_possible(3, 4) |> println(); + add_if_possible("a", "b") |> println(); +} + + +main :: (args) => { + add_example(); + struct_example(); + overloaded_procedure_example(); +} + +#load "core/std" + +use package core +use package core.intrinsics.onyx diff --git a/src/astnodes.c b/src/astnodes.c index b7a2ab4e..07307593 100644 --- a/src/astnodes.c +++ b/src/astnodes.c @@ -148,6 +148,7 @@ const char* entity_type_strings[Entity_Type_Count] = { "String Literal", "File Contents", "Enum", + "Enum Value", "Type Alias", "Memory Reservation Type", "Use", diff --git a/src/symres.c b/src/symres.c index e45d6ba0..cd525169 100644 --- a/src/symres.c +++ b/src/symres.c @@ -1403,13 +1403,16 @@ void symres_entity(Entity* ent) { next_state = Entity_State_Finalized; break; + case Entity_Type_Polymorphic_Proc: ss = symres_polyproc(ent->poly_proc); + next_state = Entity_State_Finalized; + break; + case Entity_Type_Overloaded_Function: ss = symres_overloaded_function(ent->overloaded_function); break; case Entity_Type_Expression: ss = symres_expression(&ent->expr); break; case Entity_Type_Type_Alias: ss = symres_type(&ent->type_alias); break; case Entity_Type_Enum: ss = symres_enum(ent->enum_type); break; case Entity_Type_Memory_Reservation_Type: ss = symres_memres_type(&ent->mem_res); break; case Entity_Type_Memory_Reservation: ss = symres_memres(&ent->mem_res); break; - case Entity_Type_Polymorphic_Proc: ss = symres_polyproc(ent->poly_proc); break; case Entity_Type_String_Literal: ss = symres_expression(&ent->expr); break; case Entity_Type_Struct_Member_Default: ss = symres_struct_defaults((AstType *) ent->type_alias); break; case Entity_Type_Process_Directive: ss = symres_process_directive((AstNode *) ent->expr); break; diff --git a/tests/complicated_polymorph.onyx b/tests/complicated_polymorph.onyx index 364cfd7d..9b7c468c 100644 --- a/tests/complicated_polymorph.onyx +++ b/tests/complicated_polymorph.onyx @@ -33,7 +33,7 @@ main :: (args: [] cstr) { |> map((x) => x * 3) |> add(2) |> double() - |> map((x) => cast(f32) x) + |> convert(f32) |> map((x) => x * 4 + 2) |> println(); }