Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Interesting, 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).


What 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
        }
    }


In 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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: