Interfaces define contracts: abstract sets of functions that structs must implement. Map directly to PHP interfaces.
Defining interfaces#
definterface declares one or more methods:
(definterface Describable
(describe [this] "Returns a human-readable description."))
Methods need at least this. Optional doc string follows the parameter list.
Multiple methods per interface:
(definterface Shape
(area [this] "Computes the area of the shape.")
(perimeter [this] "Computes the perimeter of the shape."))
Generates callable functions per method: (area my-shape) works like any function.
Note: Unlike PHP, Phel interfaces don't extend other interfaces.
Implementing with structs#
Only structs implement interfaces. A struct is a typed map with fixed keys, compiled to a PHP class.
Add implementations after the field list in defstruct:
(defstruct circle [radius]
Shape
(area [this] (* 3.14159 radius radius))
(perimeter [this] (* 2 3.14159 radius)))
(defstruct rectangle [width height]
Shape
(area [this] (* width height))
(perimeter [this] (* 2 (+ width height))))
Struct fields (radius, width, height) are directly accessible inside methods. No getters.
Calling methods#
Like regular functions, struct first:
(area (circle 5)) ; => 78.53975
(perimeter (circle 5)) ; => 31.4159
(area (rectangle 4 6)) ; => 24
(perimeter (rectangle 4 6)) ; => 20Multiple interfaces#
A struct can implement many. List each followed by its methods:
(definterface Describable
(describe [this]))
(defstruct circle [radius]
Shape
(area [this] (* 3.14159 radius radius))
(perimeter [this] (* 2 3.14159 radius))
Describable
(describe [this]
(str "Circle with radius " radius)))
(describe (circle 5)) ; => "Circle with radius 5"Calling other methods on same struct#
Interface dispatch routes through the generated function, not through this directly. To call another interface method on the same struct from within a method body, use the PHP method call syntax via php/-> this:
(definterface HasSummary
(summary [this]))
(defstruct product [name price]
Describable
(describe [this] (str name ": $" price))
HasSummary
(summary [this] (str "Product - " (php/-> this (describe)))))Type checking#
Each struct gets a predicate:
(circle? (circle 5)) ; => true
(circle? (rectangle 4 6)) ; => falseExample: a renderer#
Interfaces shine when types share behavior:
(definterface Renderable
(render [this]))
(defstruct paragraph [text]
Renderable
(render [this] (str "<p>" text "</p>")))
(defstruct heading [level text]
Renderable
(render [this] (str "<h" level ">" text "</h" level ">")))
(defstruct image [src alt]
Renderable
(render [this] (str "<img src=\"" src "\" alt=\"" alt "\">")))
;; Render a page from mixed elements
(let [elements [(heading 1 "Welcome")
(paragraph "Hello from Phel!")
(image "/logo.png" "Phel logo")]]
(->> elements
(map render)
(phel.string/join "\n")))
;; => "<h1>Welcome</h1>\n<p>Hello from Phel!</p>\n<img src=\"/logo.png\" alt=\"Phel logo\">"Implementing PHP interfaces#
Phel interfaces compile to PHP interfaces. Structs can implement any PHP interface:
(defstruct json-config [data]
\JsonSerializable
(jsonSerialize [this] data))Protocols#
Protocols extend functions to existing types without modifying them. Unlike interfaces (require defstruct), protocols extend to any type after the fact.
Defining#
defprotocol defines method signatures:
(defprotocol Printable
(to-string [this] "Converts the value to a printable string."))
Each method needs this. Optional doc string. Multiple methods allowed:
(defprotocol Measurable
(width [this] "Returns the width.")
(height [this] "Returns the height.")
(dimensions [this] "Returns [width height] as a vector."))Extending to types#
extend-type implements a protocol for one type:
(extend-type :string
Printable
(to-string [this] (str "\"" this "\"")))
(extend-type :int
Printable
(to-string [this] (str "int:" this)))
(to-string "hello") ; => "\"hello\""
(to-string 42) ; => "int:42"
extend-protocol implements one protocol across many types:
(extend-protocol Printable
:float
(to-string [this] (str "float:" this))
:bool
(to-string [this] (if this "true" "false")))
(to-string 3.14) ; => "float:3.14"
(to-string true) ; => "true"Checking#
satisfies? (value) and extends? (type):
(satisfies? Printable "hello") ; => true
(satisfies? Printable 42) ; => true
(extends? Printable :string) ; => true
(extends? Printable :array) ; => falseProtocols vs interfaces#
- Interfaces: when you control the type (structs), compile-time guarantees.
- Protocols: add behavior to existing types or types you don't control.
Hierarchies#
Define relationships between types or values. Hierarchies + multimethods enable inheritance-aware dispatch.
Deriving#
derive sets parent-child between keywords:
(derive :circle :shape)
(derive :rectangle :shape)
(derive :square :rectangle) ; A square is a rectangleQuerying#
isa?, parents, ancestors, descendants:
(isa? :circle :shape) ; => true
(isa? :square :rectangle) ; => true
(isa? :square :shape) ; => true (transitive)
(isa? :shape :circle) ; => false
(parents :square) ; => #{:rectangle}
(ancestors :square) ; => #{:rectangle :shape}
(descendants :shape) ; => #{:circle :rectangle :square}Removing#
underive:
(underive :square :rectangle)
(isa? :square :rectangle) ; => falseEmpty hierarchy maps#
make-hierarchy creates the empty hierarchy shape. Public derive, underive, isa?, parents, ancestors, descendants operate on the global hierarchy.
(make-hierarchy)
; => {:parents {} :descendants {} :ancestors {}}Hierarchy-aware multimethod dispatch#
Multimethods check the hierarchy for parent matches when dispatching:
(derive :circle :shape)
(derive :rectangle :shape)
(defmulti draw :type)
(defmethod draw :shape [s]
(str "Drawing a generic shape"))
(defmethod draw :circle [s]
(str "Drawing a circle with radius " (:radius s)))
(draw {:type :circle :radius 5})
; => "Drawing a circle with radius 5"
(draw {:type :rectangle :width 4 :height 3})
; => "Drawing a generic shape" (falls back to :shape via hierarchy)