← Code Compare

Generics & Polymorphism

How does a swap that flips a pair, or a map that walks any list, stay agnostic about the types it carries? The shared task is one polymorphic swap((a, b)) -> (b, a) plus a generic map, run over both numbers and strings. The real split is where the polymorphism lives: Gleam expresses it with parametric types - #(a, b) and List(a) with real type variables the compiler checks - while Erlang, Elixir, LFE, and Lua are dynamically typed, so the same function already works on any value with no annotations at all. Notice that the BEAM four pay for this freedom with zero compile-time guarantees, whereas Gleam's swap is provably correct for every a and b before it ever runs.

Show: ErlangElixirGleamLFELuerl
Erlang
-module(generic).
-export([swap/1, map/2, demo/0]).

%% No type annotations: swap/1 works on a pair of *anything*.
swap({A, B}) -> {B, A}.

%% A hand-written, fully polymorphic map over a list of *anything*.
map(_F, [])      -> [];
map(F, [H | T])  -> [F(H) | map(F, T)].

demo() ->
    {2, 1}          = swap({1, 2}),
    {ok, "x"}       = swap({"x", ok}),
    [2, 4, 6]       = map(fun(X) -> X * 2 end, [1, 2, 3]),
    ["A", "B"]      = map(fun string:uppercase/1, ["a", "b"]),
    ok.

Erlang is dynamically typed, so swap/1 and map/2 are polymorphic for free - the same clauses accept integers, atoms, or strings with no declarations. The optional -spec swap({A, B}) -> {B, A}. exists only for Dialyzer, never the compiler.

Elixir
defmodule Generic do
  # Pattern-match a 2-tuple of anything and flip it; no types needed.
  def swap({a, b}), do: {b, a}

  # Enum.map is already generic over the element type of any enumerable.
  def double_all(list), do: Enum.map(list, &(&1 * 2))
end

Generic.swap({1, 2})        # => {2, 1}
Generic.swap({"x", :ok})    # => {:ok, "x"}

[1, 2, 3] |> Enum.map(&(&1 * 2))         # => [2, 4, 6]
["a", "b"] |> Enum.map(&String.upcase/1) # => ["A", "B"]

Elixir's dynamic typing means swap/1 matches any 2-tuple regardless of what it holds, and the built-in Enum.map/2 is inherently polymorphic over the elements of whatever it iterates. Polymorphism here is the default, not a feature you opt into.

Gleam
import gleam/io
import gleam/int
import gleam/list
import gleam/string

// Real type variables: swap is generic over *every* a and b,
// and the compiler proves the result type is #(b, a).
pub fn swap(pair: #(a, b)) -> #(b, a) {
  let #(first, second) = pair
  #(second, first)
}

pub fn main() {
  // #(Int, String) flips to #(String, Int) - checked at compile time.
  let #(s, n) = swap(#(1, "x"))
  io.println(s <> "/" <> int.to_string(n))
  // prints: x/1

  // list.map has type fn(List(a), fn(a) -> b) -> List(b).
  [1, 2, 3] |> list.map(fn(x) { x * 2 }) |> echo
  // => [2, 4, 6]
  ["a", "b"] |> list.map(string.uppercase) |> echo
  // => ["A", "B"]
}

Gleam uses parametric polymorphism: the lowercase a and b in #(a, b) -> #(b, a) are type variables, so one definition is sound for all types and the compiler verifies each call site. list.map's signature fn(List(a), fn(a) -> b) -> List(b) likewise tracks the element type through the transform.

LFE
(defmodule generic
  (export (swap 1) (map 2) (demo 0)))

;; Match a 2-tuple of anything and return it flipped.
(defun swap
  (((tuple a b)) (tuple b a)))

;; A polymorphic map written with recursion over the list.
(defun map
  ((_f ()) ())
  ((f (cons h t)) (cons (funcall f h) (map f t))))

(defun demo ()
  (let* ((p   (swap (tuple 1 2)))          ; => #(2 1)
         (q   (swap (tuple "x" 'ok)))      ; => #(ok "x")
         (xs  (map (lambda (x) (* x 2)) '(1 2 3)))      ; => (2 4 6)
         (ys  (map #'string:uppercase/1 '("a" "b"))))   ; => ("A" "B")
    (list p q xs ys)))

LFE is Erlang under Lisp syntax, so swap simply pattern-matches (tuple a b) and rebuilds it flipped, with no types in sight. The hand-rolled map recurses with (cons h t) and funcall, accepting any element type because the runtime never checks one.

Luerl
-- A pair is just a 2-element table; swap flips it, type-agnostic.
local function swap(pair)
  return { pair[2], pair[1] }
end

-- A generic map: works on a sequence of anything because Lua
-- values are dynamically typed and functions are first-class.
local function map(fn, t)
  local out = {}
  for i, v in ipairs(t) do out[i] = fn(v) end
  return out
end

local p = swap({ 1, 2 })            -- { 2, 1 }
local q = swap({ "x", "ok" })       -- { "ok", "x" }
print(p[1], p[2], q[1], q[2])       --> 2  1  ok  x

local doubled = map(function(x) return x * 2 end, { 1, 2, 3 })
local upper   = map(string.upper, { "a", "b" })
print(doubled[1], doubled[2], doubled[3]) --> 2  4  6
print(upper[1], upper[2])                 --> A  B

Lua has no static types, so swap and map are polymorphic by nature - a table can hold any mix of values and a function value can be passed straight in. There is nothing to parameterise: every function is already generic over whatever you hand it.