Requires (Generics/Type Bounds)
Introduction to requires
Introduction to requiresThe requires
keyword in Helix enables powerful generics and type constraints. By explicitly defining what types or constraints are allowed, it ensures robust and reusable constructs. The requires
keyword can be used in the following constructs:
Syntax Overview
Syntax OverviewThe requires
keyword introduces generic parameters, optionally constrained by types or conditional logic.
Grammar Reference
Grammar ReferenceThis grammar indicates:
- A
requires
clause defines one or more generic parameters. - Parameters can optionally include:
const
for constant values.- A constraint (
:
Type) specifying the expected type.
- A
requires
clause may optionally be followed by aTypeBoundDecl
for additional type contracts/bounds logic usingif
.
Using requires
in Constructs
Using requires in ConstructsFunctions
FunctionsYou can attach the requires
clause to a function definition after its signature to define generic functions:
- Explanation:
T
is a generic type.- The function works only for types that implement the
Comparable
interface. - For more info on
has
,derives
refer to Type System.
For a more generic function (without constraints), would only take the requires
clause:
- Explanation:
- The function accepts any type
T
.
- The function accepts any type
Structs
StructsThe requires
clause enables generics for structs:
- Explanation:
Container
holds a value of typeT
.
Adding Constraints to Structs
Adding Constraints to Structs- Explanation:
T
must be of typeStorable
.
If we want a slightly less strict constraint, we can use derives
:
- Explanation:
T
must derive fromStorable
. (OOP inheritance hierarchy)
Classes
ClassesSimilar to structs, classes can use the requires
clause for generics:
- Explanation:
K
must be implementHashable
. Refer to Interfaces for more on how to define and implement interfaces.V
is a value type without additional constraints.
Operators
OperatorsOperators can also use requires
to generalize their behavior:
- Explanation:
- This generic operator works for any type
T
that supports the subtraction operator.
- This generic operator works for any type
Type Definitions
Type DefinitionsThe requires
clause can define generic type aliases:
- Explanation:
Pair
is a generic type holding two related values.- Usage:
let p: Pair::<int, string> = (10, "Helix");
Conditional Constraints
Conditional ConstraintsHelix allows logical expressions in requires
clauses to define more complex constraints.
Example: Logical Constraints
Example: Logical Constraints- Explanation:
- The function accepts only numbers or strings.
Example: Combining Multiple Parameters
Example: Combining Multiple Parameters- Explanation:
N
andE
are constrained to types derivingNode
andEdge
, respectively.
Type Bounds (Advanced)
Type Bounds (Advanced)The if
keyword in the requires
clause allows you to specify type bounds for generic parameters.
They can be any valid expression that evaluates to a boolean value. For example:
- Explanation:
- The
CheckType
struct has two overloads based on theT
type beingconst
or not. - The
only_const
function only works withconst
types since if a type thats const is passed it would call the first overload ofCheckType
that setsis_const
totrue
. - The
only_not_const
function only works with non-const
types since if a type thats not const is passed it would call the second overload ofCheckType
that setsis_const
tofalse
and negates it.
- The
Checking the Type of a Generic outside the requires
Clause
Checking the Type of a Generic outside the requires ClauseYou can check the type of a generic parameter outside the requires
clause using the has
or derives
operator:
- Explanation:
- The function converts the generic value to a string based on its type.
- The
eval if
construct allows conditional logic based on the type ofT
. - At compile-time, the correct branch is chosen based on the type of
T
. - This is a powerful zero-cost abstraction that ensures type safety. While being as dynamic as python with types.
Refer to Type System for more about helix’s powerful type system.
Examples and Use Cases
Examples and Use CasesCommon Errors and Pitfalls
Common Errors and Pitfalls-
Ambiguous Constraints: Avoid ambiguous constraints that can lead to compile-time errors. For example:
- This would result in an ambiguous call error.
- Since both functions take a generic, but the first function would be called for any type, and the second function would be called for any type that implements
Displayable
. - This would result in a compile-time error since the compiler wouldn’t know which function to call. As both functions are valid for a type that implements
Displayable
. - To fix this, you can either remove the first function or add a constraint to the first function that would make it more specific than the second function.
-
Overly Specific Constraints: Avoid constraints that are too specific and limit the reusability of your code. For example:
- This would limit the
Box
struct to only work withint
’s and not any other type (even if it derives fromint
). - To fix this, you can remove the constraint or make it more general. Or add a type bound that would allow any type that derives from an
int
.
- This would limit the
- Define Clear Constraints: Always specify meaningful constraints to make your code more readable and robust.
- Use Conditional Logic Where Needed: Combine
if
with logical operators to express nuanced requirements. - Document Every Generic Parameter: Clarify the purpose of each generic type and constraint in your code comments.
Conclusion
ConclusionThe requires
keyword in Helix is a powerful tool for defining generic types and constraints. By specifying the expected types and conditions, you can ensure type safety and robustness in your code. Use requires
in functions, structs, classes, operators, and type definitions to create reusable and flexible constructs.