Dynamic Typing and Polymorphism

Learn what Clojure’s dynamic typing really means, how polymorphism works through functions, protocols, and multimethods, and how to design flexible code without losing clarity.

Clojure is dynamically typed, but that does not mean “types do not matter.” It means types belong to values at runtime rather than being fixed in variable declarations. That design encourages flexible generic code, but it also shifts some responsibility from the compiler to the programmer’s data modeling and API design.

Dynamic typing: A model where values carry types at runtime, while variables and parameters are not declared with fixed types in advance.

Understanding that clearly is important because many newcomers hear “dynamic typing” and assume anything can be mixed with anything safely. That is not how Clojure works. Operations still have expectations. They are just enforced at runtime instead of through mandatory declared types.

Types Belong to Values, Not Variables

This is valid Clojure:

1(def x 42)
2(def x "now a string")

The name is rebound to a different value. The language does not require a static type declaration for x.

But functions still operate meaningfully only on appropriate inputs:

1(+ 2 3)
2;; => 5
3
4(+ "hello" "world")
5;; error

Dynamic typing does not erase semantic expectations. It changes when and how those expectations are enforced.

Genericity Often Comes from Data Orientation

One of Clojure’s strengths is that many functions work across broad families of data rather than across tightly declared class hierarchies.

For example, sequence functions work with many collection types:

1(map inc [1 2 3])
2(map inc '(1 2 3))
3(map inc #{1 2 3})

This kind of genericity often comes from shared interfaces and protocols implemented by the runtime, not from the caller declaring types ahead of time.

Polymorphism Has More Than One Shape in Clojure

In Clojure, polymorphism does not mean only “call a method on an object.” It can take several forms:

  • plain generic functions over common data abstractions
  • higher-order functions that accept behavior explicitly
  • protocols for efficient type-based dispatch
  • multimethods for value- or hierarchy-based dispatch

Choosing among these is a design decision.

Start with Plain Functions

Many problems that look like polymorphism do not need a polymorphism framework.

1(defn full-name [{:keys [first-name last-name]}]
2  (str first-name " " last-name))

If the data shape is already enough, a plain function is often the most idiomatic choice. Clojure code gets simpler when behavior is expressed directly over data.

Use Protocols for Type-Oriented Behavior

Protocols are the right tool when behavior is tied to a type-like abstraction and dispatch should happen on the first argument’s type.

1(defprotocol Drawable
2  (draw [this]))
3
4(defrecord Circle [radius]
5  Drawable
6  (draw [_] (str "Circle with radius " radius)))

Protocols are useful because they:

  • give clear named behavior contracts
  • dispatch efficiently
  • work well when you own the types or interfaces involved

They are not the only form of polymorphism, and they are not automatically needed whenever behavior varies.

Use Multimethods When Dispatch Is About Values or Hierarchies

Sometimes behavior depends on more than type. It may depend on a keyword, a category, a status, or a custom hierarchy.

1(defmulti handle-event :kind)
2
3(defmethod handle-event :created [event]
4  :persist)
5
6(defmethod handle-event :deleted [event]
7  :archive)

Multimethods are especially useful when:

  • dispatch depends on value, not just type
  • dispatch depends on multiple properties
  • custom derive hierarchies help model the domain

They are more flexible than protocols, but usually less direct and heavier than plain functions.

Dynamic Typing Needs Better Boundaries, Not Fear

Because many guarantees are not enforced at declaration time, good Clojure design relies more on:

  • clear data shapes
  • validation at system boundaries
  • explicit error handling
  • smaller, composable functions
  • choosing the lightest polymorphism tool that fits

Dynamic typing becomes dangerous mostly when boundaries are muddy and assumptions are left implicit.

A Useful Decision Order

    graph TD;
	    A["Behavior varies"] --> B{"Plain function over data enough?"}
	    B -->|Yes| C["Use ordinary functions"]
	    B -->|No| D{"Dispatch by first-arg type?"}
	    D -->|Yes| E["Use a protocol"]
	    D -->|No| F["Use a multimethod or other value-based dispatch"]

The diagram below captures the most useful habit in Clojure: start simple, then move to protocols or multimethods only when the dispatch pressure is real.

Quiz

Loading quiz…
Revised on Thursday, April 23, 2026