Generic code often needs an operation that belongs to a type rather than to any instance — 'produce the identity value', 'parse one of these from text', 'give me an empty builder'. Some languages let an interface or constraint require such a member; others cannot express it at all. Compare the designs and the price each one pays.
answer
- no receiver -> something else must choose the impl
- Haskell mempty: dispatch on the RESULT type
- Rust: Self-returning trait fn = not object-safe, no dyn
- C# 11 static abstract: through a type parameter only
- Go's answer: make the zero value useful
basics
~20 sRequiring a type-level operation means dispatching without a receiver. Haskell picks the implementation from the expected result type via dictionaries; Rust and Swift allow such requirements but the trait or protocol stops working as dyn/existential; C# 11 adds static abstract members usable only through a type parameter; Java must hand in a factory object.
solid answer
~60 sThe operation has no instance to dispatch on, so something else must carry the choice of implementation. - **Haskell**: `mempty :: a` takes no argument at all — the instance is selected from the *result* type by dictionary passing. Price: ambiguity when the result type is not inferable, resolved with annotations or `TypeApplications`. - **Rust**: a trait may declare `fn new() -> Self`, but that makes the trait non-object-safe, so it is usable through generics and `impl Trait` and never through `dyn Trait`. The orphan rule additionally forbids implementing a foreign trait for a foreign type. - **Swift**: protocols with `static` or `Self` requirements hit the same fork on existentials, but Swift permits retroactive conformance where Rust refuses it. - **C# 11**: `static abstract` interface members (the basis of `INumber<T>.Zero`) are reachable through a type parameter and never through an interface-typed reference. - **Kotlin/Scala**: no statics at all — a companion `object` implementing an interface turns the type-level operation into an ordinary value; Scala's `given` instances are typeclasses resolved at compile time. - **Go**: no constructor abstraction; the zero value is the language's answer, which is why "make the zero value useful" is idiom.
code
haskell · 6 linesclass Monoid a where
mempty :: a -- no argument: chosen by the expected result type
mappend :: a -> a -> a
total :: Monoid a => [a] -> a
total = foldr mappend mempty -- works for [] as wellgo deeper
Recognise the situation — code that needs a value of a type before it has any instance of that type — and that a factory has to come from somewhere.
Be able to say why receiver dispatch cannot help and name one language that requires the member in an interface (C# 11) and one that cannot (Java).
Explain the object-safety / existential cost concretely, and choose between a typeclass-style constraint and an injected factory based on whether polymorphism is compile-time or runtime.
Trade coherence against extensibility explicitly — Rust's orphan rule versus Swift or Clojure retroactive extension — and pick the abstraction style your dispatch requirements actually allow.
## The shape of the problem Write a function that folds a collection into a summary. To fold an *empty* collection you need a starting value — the identity element. Nothing in the collection can supply it, because the collection is empty. The value belongs to the element *type*, not to any element. The same shape appears for "parse one of these from a string", "give me the default configuration", "construct an empty accumulator of this kind". Instance-level dispatch cannot help: dispatch normally starts from a receiver, and here there is no receiver. Languages answer this in four distinguishable ways, and each answer has a cost you can state precisely. ## Answer 1: dispatch on the result type (Haskell) A Haskell typeclass method need not mention its class variable in an argument position. `mempty :: a` mentions it only in the result. The compiler chooses the instance from the type the expression is *expected* to have, and passes the corresponding dictionary at the call site. So `foldr (<>) mempty xs` works for any monoid, empty list included. The cost is that when the result type is not determined by context, the program is ambiguous and needs an annotation or an explicit type application. Also, instance resolution is global: two libraries defining the same instance for the same type are a coherence problem, which Haskell prevents by convention and orphan-instance warnings. ## Answer 2: allow the requirement, lose the existential (Rust, Swift) Rust lets a trait declare associated functions with no `self`: `fn default() -> Self`, `fn from_str(s: &str) -> Result<Self, E>`. Generic code constrained by that trait can call `T::default()`, and the compiler monomorphises. But such a trait is not *object-safe*: you cannot make a `dyn Trait` out of it, because a trait object is a pointer plus a vtable for a value that already exists, and "produce a value of the implementing type" has no value to start from and no single answer to compile into a slot. The price is concrete: this family of abstraction is static-dispatch only. Swift's protocols hit the same wall from the other direction: a protocol with `static` requirements or `Self` requirements historically could not be used as a type, only as a generic constraint. Swift and Rust then diverge on *who may add conformance*: Swift permits retroactive conformance of a type you do not own to a protocol you do not own; Rust's orphan rule forbids it precisely to guarantee that a type has at most one implementation of a trait program-wide. ## Answer 3: put the requirement on the type parameter (C# 11) C# 11 introduced `static abstract` interface members. `INumber<T>` declares `static abstract T Zero { get; }`, and generic math code writes `T.Zero`. Dispatch happens through the type argument at JIT specialisation time — so, exactly like Rust, it works through a constrained type parameter and not through an interface-typed reference. The design is a deliberate acceptance of the same trade: type-level requirements are compatible with generics, not with existentials. ## Answer 4: turn it into an ordinary object Kotlin and Scala have no statics; a companion object or a Scala `object` is a real singleton that can implement an interface. "The type-level operation" therefore becomes an ordinary instance method on an ordinary value you can pass, store and mock — at the cost that the companion is not inherited by subclasses, so it is a value you route explicitly rather than something the hierarchy hands you. Scala pushes further with `given`/`implicit` instances, which are typeclass dictionaries the compiler finds and passes for you: Haskell's mechanism with an explicit escape hatch to supply a different instance locally. Java cannot express the requirement at all, so the same idea appears as a `Supplier` or a strategy object handed in by the caller — the workaround that follows directly from a static having no receiver. Go declines the whole question: every type has a zero value, `new(T)` produces it, and the idiom "make the zero value useful" exists so that the language never needs a per-type constructor abstraction. ## Choosing The useful decision rule falls out of the mechanism rather than taste. If you need runtime polymorphism — a heterogeneous list, a plugin loaded by name — you cannot put the type-level operation in the abstraction; you need a separate factory value, and languages with existentials will force that on you. If your polymorphism is compile-time — generic numeric code, serialisation over known types, parser combinators — the typeclass/constraint route is stronger, because the identity element and the parser come from the type itself and cannot be forgotten by a caller. The second decision is coherence versus extensibility: Rust's orphan rule buys you "one implementation, globally", while Swift and Clojure buy you "extend types you do not own" and accept that two libraries can disagree.
- Why exactly can a trait containing `fn new() -> Self` not be used as `dyn Trait`?A trait object is a pair of a data pointer and a vtable belonging to one already-existing value. Calling `new()` requires choosing an implementing type before any value exists, and a `dyn Trait` has erased that choice. There is nothing to look the function up on, so the compiler excludes such members from object safety rather than allowing a call that could not be resolved.
- Kotlin has no statics, yet you can still not require a type-level operation in an interface the way C# 11 does. How do people close that gap?They make the companion object implement an interface — for example a `Parser<T>` interface implemented by `Foo.Companion` — and then pass that companion as an ordinary value wherever the operation is needed. The abstraction is real and mockable, but the routing is manual: the caller must supply the companion, since nothing in the type system connects a type to its companion for generic code.
saying these in an interview costs you the question
- Saying Java 'just uses a static factory' as if that were an abstraction — no generic code can require it.
- Assuming a Rust trait with associated functions can be used as dyn Trait, then blaming the borrow checker for the error.
- Thinking C# 11 static abstract members work through an interface-typed reference rather than through a type parameter.
- Confusing Haskell's result-type dispatch with ordinary receiver dispatch and expecting a first argument to select the instance.
- Treating Swift retroactive conformance and Rust's orphan rule as the same policy.