Haxe Code Cookbook
Haxe programming cookbookOtherPassing different types to a function parameter

Passing different types to a function parameter

Reading time: 4 minutes

Sometimes you find yourself in the need of passing different types to the same parameter of a function. While there isn't a built-in way of doing this in Haxe, thanks to its flexible type system, we can solve this problem. We will explore two ways to achieve this.

Access fields that are present in all our types

Let's say, for example, that we want to get the length of an Array<T> or an String:

function length(o:Array<Int>/*or String*/):Int {
  return o.length;
}

That function won't work for us, it will only work for Arrays so we need to find a way to fix it. The laziest and more dangerous method we can use is Dynamic.

function length(o:Dynamic):Int {
  return o.length;
}

The problem with this method is, as the manual says, that we can pass anything to the function and the compiler won't care about it, leaving any error or exception to the target language.

Getting rid of Dynamic

Isn't there a safer way of doing this so we can catch any error at compile time? We know that both types have a length(default, null):Int field and we are only interested on the length field. We can then use type parameter constraints.

function length<T:{var length(default, null):Int;}>(o:T) {
  return o.length;
}

Or, if we are going to use the constraint in more functions, we can use a typedef

Haxe Manual: A typedef can give a name to any other type and/or describe a complex structure.

typedef Length = {
  var length(default, null):Int;
}

function length<T:Length>(o:T) {
  return o.length;
}

function length2<T:Length>(o:T) {
  return o.length * 2;
}

Of course, this will only work if we can constraint the types somehow. But what if we can't?

Using different types

Now, we need a function that takes an Array<Int> or an Int so we can return an Array<Int> after adding a few numbers. If we pass an Array it will return the same Array with the new numbers added. If we pass an Int it will create an Array<Int> from 0 to the value passed and add the new numbers to it. The problem here is we can't use the same method we used before because there isn't any constraint we can use for these types.

We can try to use Dynamic again, but as we saw before it isn't type safe and error-prone: All the errors or exceptions will be at runtime and the compiler won't warn us if we pass another invalid type.
We will also lose the type of the Array (Int) because this information is only used at compile time and erased at runtime. This is why we can't use Std.is(o, Array<Int>).

function add(o:Dynamic):Array<Int> {
  var a:Array<Int>;
  if(Std.is(o, Array)) {
    a = cast o;
  } else if(Std.is(o, Int)) {
    a = [for(i in 0...Std.int(o)) i];
  } else {
    throw "The value isn't an Array or an Int";
  }

  a.push(100);
  a.push(300);

  return a;
}

Making it more type safe with Either

There is a more type safer way that let the compiler catch possible errors at compile time. We can use haxe.ds.Either. It has to be mentioned that Either gives some runtime performance overhead, but it brings type safety which we are looking for.

Haxe Manual: Either represents values which are either of type L (Left) or type R (Right).

function add(o:haxe.ds.Either<Array<Int>, Int>):Array<Int> {
  var a:Array<Int>;
  switch(o) {
    case Left(l): a = l;
    case Right(r): a = [for(i in 0...r) i];
  }

  a.push(100);
  a.push(300);

  return a;
}

Now the add function can be used like demonstrated here:

trace("Passing an Array<Int>: " + add(Left([10, 20, 30])));
trace("Passing an Int: " + add(Right(3)));

Great! The code is now type safe and the compiler will error if we give it the wrong types, but isn't it a bit cumbersome? We need to remember that if we want to pass an Array<Int> we need to use Left() and if we want to pass an Int we need to use Right().

Let's make it easier to work with!

Taking it further with abstract types

This method will solve the problems every other method had. It's based on the last method but, thanks to the powerful Haxe abstract types, we can let the compiler do the job for us.

Haxe Manual: An abstract type is a type which is actually a different type at run-time. It is a compile-time feature which defines types "over" concrete types in order to modify or augment their behavior.

First of all we need to write the abstract. By defining the @:to and @:from functions we define the casts to/from one to another type.

import haxe.ds.Either;

abstract OneOf<A, B>(Either<A, B>) from Either<A, B> to Either<A, B> {
  @:from inline static function fromA<A, B>(a:A):OneOf<A, B> {
    return Left(a);
  }
  @:from inline static function fromB<A, B>(b:B):OneOf<A, B> {
    return Right(b);  
  } 
    
  @:to inline function toA():Null<A> return switch(this) {
    case Left(a): a; 
    default: null;
  }
  @:to inline function toB():Null<B> return switch(this) {
    case Right(b): b;
    default: null;
  }
}

We can now modify our add() function to:

function add(o:OneOf<Array<Int>, Int>):Array<Int> {
  var a:Array<Int>;
  switch(o) {
    case Left(l): a = l;
    case Right(r): a = [for(i in 0...r) i];
  }

  a.push(100);
  a.push(300);

  return a;
}

Nice! We get type safety. We will get compile time errors if we by accident pass incorrect types to our method. The compiler does the dirty job for us.


You may be wondering "what if I want to use more types?"
Well, haxe.ds.Either is just an enum (see its source code) so nothing stops you from writing an enum with more type parameters and modify OneOf to accept more @:from and @:to.

Of course, you can always use macros to create it and the abstract but we will leave that for another day.

Learn more:


Contributors:
Last modified:
Created:
Category:  Other