There are two cases for me, either the function describes its input and the caller must provide them in the correct shape, or the function leverages an existing application entity (data model), and should refer to its definition by name.
In the case where a function doesn't operate over an entity, I make sure to have a good argument name, to describe the shape in the doc-string and to use destructuring if taking a map or tuple to name each element. Sometimes I might also have a spec for it, though that's really only when I want to also have generative tests or do validation with it.
In the case where a function operates over an entity, I make the name of the argument the same as the name of the entity definition.
I define my entities either using a constructor, a function called make-foo thus creates an instance of an entity of conceptual type foo. That function will document the shape of what it creates, or it'll be pretty easy to figure it out from looking at the code for it. Or I define them using a record where the record definition describes the shape. Or I define them using a Spec with the spec name the name of the entity.
(defn make-item
"Makes an item which is a map of key
:id UUID of the todo-list
:name - the user readable string name of the item
:content - the user provided string content of the item"
[name content]
{:id (java.util.UUID/randomUUID)
:name name
:content content})
(defn make-todo-list
"Makes a todo-list which is a map of key
:id - UUID of the todo-list
:name - the user readable string name of the list
:items - an ordered vector of item"
[name & items]
{:id (java.util.UUID/randomUUID)
:name name
:items (vec items)})
(defn insert-item
"Inserts an item in a todo-list at given 0-based position"
[todo-list item position]
(apply conj
(subvec todo-list 0 position)
item
(subvec todo-list position (count todo-list))))
(defn sanitize-input
"Given an untrusted input string, return a sanitized version.
Takes an optional options map with options:
:extra-denylist - a seq of string words to delete on top of default sanitization."
[input-string & {:keys [extra-denylist] :as options}]
(some-lib/sanitize input-string extra-denylist)
So as you can see in my little example, sanitize-input
is an example of a function that doesn't operate over the application domain model, so it just describes its input using good argument names, destructuring and a nice doc-string.
On the other hand, insert-item
is a function that operates over the application domain model, so it just uses the same name as the domain entities it takes as input, and its extra argument position
is simply described by a good name and the doc-string.
Finally the domain entities are described by the make-
functions of their respective names. I could have used a record in their place, or a Spec as well.