This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-08-21
Channels
- # announcements (20)
- # beginners (31)
- # biff (8)
- # cherry (5)
- # cider (4)
- # cljs-dev (1)
- # clojure (26)
- # clojure-australia (2)
- # clojure-europe (16)
- # clojure-spec (10)
- # community-development (8)
- # conjure (1)
- # core-async (1)
- # data-oriented-programming (1)
- # data-science (54)
- # datascript (10)
- # fulcro (1)
- # graalvm (2)
- # malli (5)
- # off-topic (3)
- # pathom (23)
- # rdf (1)
- # re-frame (6)
- # reitit (11)
- # shadow-cljs (6)
- # squint (2)
- # xtdb (33)
after reading a book called Domain Modeling Made Functional I wonder what would be a good way to attach type information to pieces of data. I'm not talking about a type system, but about reasoning of how data flows through components. I'd like have specs that claim a function only accepts data that went through some kind of processing. E.g., an email is parsed and validated on the edge of the system, but not confirmed yet by the user, and I'd like to make sure it's never used inappropriately. How would you do this? Using keywords (with inheritance, probably) or with records?
Seems like a perfect use of metadata
> I'd like have specs that claim a function only accepts data that went through some kind of processing. E.g., an email is parsed and validated on the edge of the system, but not confirmed yet by the user, and I'd like to make sure it's never used inappropriately
This tends to be pretty trivial in Clojure and with Spec. You can use metadata, but I'd say in general, you don't even have too, since you'll probably be working with maps only for your model anyways, you can just add more data to them.
For example, on your User entity (which should be a plain Clojure map), just add a :type
key where the values are the various state of a User through your system.
So maybe you have a {:type :verified-user}
and a {:type :registered-user}
.
Now in each function, where you expect a :verified-user
, well just have a spec for that which will also assert that :type
is :verified-user.
This will probably be pretty close to the book you read, where I'm assuming they use custom ADT types to model this.
Alternatively, you can enhance the user with more info, and validate the info, such as adding an :email-verified true
on the User only after its been verified, then have a spec on the function that asserts that :email-verified
exists and is true on the user.
You could make this metadata, but it's harder to use spec to validate meta, not super hard, but you kind of need two specs, one for the data and one for the meta. The advantage of meta is if you don't want them to participate in equality. Like if you still want two users to be equal even if one is verified and the other is not.
(require '[com.myapp :as-alias app])
(require '[com.myapp.user :as-alias user])
(s/def ::user/type #{:registered-user :verified-user})
(s/def ::user/username string?)
(s/def ::user/email string?)
(s/def ::app/user
(s/keys :req-un [::user/type ::user/username ::user/email]))
(s/def ::app/verified-user
(s/and ::app/user #(= :verified-user (:type %))))
(s/def :app/registered-user
(s/and ::app/user #(= :registered-user (:type %))))
So now on functions that expect a :verified-user you can do:
(s/fdef somefn
:args (s/cat :verified-user ::app/verified-user)
:ret ::app/verified-user)
And on the function that verifies a registered-user you can do:
(s/fdef verify-user
:args (s/cat :registered-user ::app/registered-user)
:ret ::app/verified-user)
And if different types of users also had different set of keys, you can multi-spec the ::app/user spec.
@U0K064KQV, oh, this is embarassing. I've just opened this thread five months after your answer. Thanks, looking forward to actually try this approach.