I am designing a library to wrap an API with Clojure. The API requires user credentials to authenticate user related calls.
My first approach was to have functions that do each task the API can do:
(defn send-email
[credentials email]
(call-api credentials :email email))
(defn send-message
[credentials message]
(call-api credentials :say message))
However, I noticed that most of the functions have the same argument, credentials
. Hence, I thought of another way to approach this. How about returning functions partial
ed with the credentials after you login:
(defn authenticate
[username password]
(let [credentials (api-auth username password)]
{:credentials credentials
:send-email (partial send-email credentials)
:send-message (partial send-message credentials)})
This makes it much more convenient, the user of this library does not need to add the credentials on every call.
But wait, this looks a lot like what you would do in object-oriented programming. You pass something into a function (the initializer) and you get functions (from an object) that are tailored to what you passed in.
The above could have been written as this in Java:
public class Api {
private String credentials;
Api(String username, String password) {
this.credentials = Utils.api_auth(username, password);
}
void send_email(String email) {
Utils.call_api(this.credentials, Utils.EMAIL, email);
}
void send_message(String message) {
Utils.call_api(this.credentials, Utils.SAY, message);
}
}
As Clojure is a functional programming language, I want to avoid object-oriented programming but it feels as though the whole task was meant for object-oriented programming languages.
How should I design for such an API? Which approach should I choose, or is there something I am not thinking of in my object-oriented mind?
UPDATE
I thought of a third way: binding
s:
(def ^:dynamic *credentials*)
(defn send-email
[email]
(call-api *credentials* :email email))
(defn send-message
[message]
(call-api *credentials* :say message))
; Usage
(binding [*credentials* (api-auth username password)]
(send-email "hello")
(send-message "hello"))
However, the problem with this is that it is very easy to create erroneous code—just don't bind anything at the start. This is different from implementations of dynamic vars I have seen (pprint
), where there is a default binding.
-
Wrap it in a protocol if you want to feel more Clojurey.kdgregory– kdgregory2017年08月08日 00:08:58 +00:00Commented Aug 8, 2017 at 0:08
-
@kdgregory according to the linked page, protocols are meant for abstractions. That means that it would be useful if I had more than one type of credentials... but I don't.Moon Cheesez– Moon Cheesez2017年08月11日 12:35:41 +00:00Commented Aug 11, 2017 at 12:35
-
1@MoonCheesez "this looks a lot like what you would do in object-oriented programming" So what?Stop harming Monica– Stop harming Monica2017年12月09日 17:38:54 +00:00Commented Dec 9, 2017 at 17:38
-
1@MoonCheesez Your clojure code is pure, isn't it?Stop harming Monica– Stop harming Monica2017年12月11日 08:02:31 +00:00Commented Dec 11, 2017 at 8:02
-
1Just FTR: (1) Yes, "objects" are families of partially-applied functions, usually namespaced under that object; (2) No, the partially-applied state is not required to be mutable. (3) Objects are not a problem; uncontrolled mutable state and uncontrolled effects are. You can have objects that play nice both with state (e.g. immurtable) and with effects (e.g. in a monadic way).9000– 90002020年01月29日 17:20:12 +00:00Commented Jan 29, 2020 at 17:20
2 Answers 2
OO requires objects have mutable attributes
No, OO requires objects have attributes. There is a lot of OO code that uses objects with mutable attributes, and also a lot of OO code that uses objects with only immutable attributes.
Take a look at Java's String
class, all the operations give you new String
s, rather than mutating the content of this
.
Not sure if it is "functional" enough for you but you could replace call-api
with call-auth-api
and in that have it obtain credentials, possibly from a duration limited store, and supply them as one of the inputs to the actual call-api
.
-
I think implementing a duration limited store can increase the complexity greatly as managing the store, especially expiring the store, will require a constant checking. Aside from that, pulling data from an external source in a function is not really functional...Moon Cheesez– Moon Cheesez2017年08月11日 12:46:45 +00:00Commented Aug 11, 2017 at 12:46
Explore related questions
See similar questions with these tags.