4 ms·
What you want to do is possible with a structural type system. In structural type systems, type compatibility is based on the structure of the type instead of t
by frankpf 9y ago
What you want to do is possible with a structural type system. In structural type systems, type compatibility is based on the structure of the type instead of the name (as opposed to nominal type systems).
An example in TypeScript:
interface Named {
name: string
}
class Person {
name: string
age: number
// constructor here
}
function f(obj: Named) {
// do something with obj.name
}
const joe = new Person('Joe', 25)
// Compiles, even though Person has an extra `age` field
// Person is structurally compatible with Named
f(joe)
If you pass f() a class/object that doesn't have a `name` property of type string, the compiler would catch your error.
- vikiomega9 9y agoThis is not easy to do in say Java which is significantly different from clojure (or how typescript enforces typing). In your example it appears that you're using the name field rather than the type Named to shape data that the function will use (this is in essence ducktyping)
- andrewflnr 9y agoRight, but that's nothing to do with types in general, mostly just Java and friends with unimaginative type systems.
- frankpf 9y agoI am using `Named` as an argument. In structural type systems, what matters is the type structure. It's not duck typing because it's checked at compile-time. If I change the type of "Named" to have the fields `firstName` and `lastName` of type string, accessing any other property inside `f` (like `obj.nonExisting`) or passing objects that don't have those fields.
- vikiomega9 9y agothanks! just to clarify, how do you manage the composition of these types? Do you create a structural type per API and potentially have the "same" types repeated multiple times?
- nogridbag 9y agoInteresting, except you're redefining the type of "name" in each each class by specifying "string". Also, Clojure.spec allows you to be much more precise about properties. For example, it must be a certain length, non-nil, or even match a regex (e.g. first letter starts with uppercase).
- frankpf 9y agoWhat do you mean by redefining? I'm not redefining the type of "name". If you want to get compile-time checking that `Person` implements `Named`, you can use `class Person implements Named`. Even better than that, you don't need to use classes, you can use "normal" JS data structures. Extending the last example: const john = { name: 'John', age: 25 } // No class involved f(john) // compiles const alice = { age: 25 firstName: 'Alice', lastName: 'Jones' } f(alice) // compile-time error Clojure.spec is cool, but I don't see how that is incompatible with static typing. You can still have libraries that check more complex properties at runtime. EDIT: > Also, Clojure.spec allows you to be much more precise about properties. For example, it must be [...] non-nil [...] TypeScript also handles nulls in the type system: function f(x: string | null) { if (x != null) { // tsc knows that inside this if, x can't be null return x.length } else { console.log(x.length) // this doesn't compile, x is of type null here } }
- nogridbag 9y agoIn Clojure.spec, you spec out namespaced keywords once. (def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") (s/def ::email-type (s/and string? #(re-matches email-regex %))) (s/def ::acctid int?) (s/def ::first-name string?) (s/def ::last-name string?) (s/def ::email ::email-type) Using those keywords you can define maps which specify shapes of data: (s/def ::person (s/keys :req [::first-name ::last-name ::email] :opt [::phone])) Functions have their own separate specifications. Here's one that accepts a person and an acctid: (s/fdef add-to-account :args (s/cat :person ::person :acctid ::acctid) And must be called like this: (add-to-account {::first-name "John" ::last-name "Smith" ::email "john@smith.com"} 12345) If you tried calling it with an illegal argument and it's instrumented you will see an error: (add-to-account {::first-name "John" ::last-name "Smith" ::email "abc123"} 12345) ExceptionInfo Call to #'scratch.core/add-to-account did not conform to spec: In: [0 :scratch.core/email] val: "abc123" fails spec: :scratch.core/email-type at: [:args :person :scratch.core/email] predicate: (re-matches email-regex %) Here's the difference. What if I have another function that just accepts an email: (s/fdef lookup-user :args (s/cat :email ::email) And another which looks up by last name: (s/fdef lookup-user-by-name :args (s/cat :last-name ::last-name) And now imagine if you wanted to accept either an email or last-name (notice it does not match our person spec). Attempting to use interfaces would quickly get out of control. You'd have to create an interface for every single property and extend interfaces to form arbitrary groups of properties.