[erlang-questions] Exporting a record type

Jesper Louis Andersen jesper.louis.andersen@REDACTED
Sun Jul 12 14:31:24 CEST 2015


On Fri, Jul 10, 2015 at 6:11 PM, <lloyd@REDACTED> wrote:

> So, now what syntax would I use in this module to populate the book record?


So, we have a book record in a database. We want a representation of that
book record in the system. In other words, we want to be able to reify a
book in the database as a book in the system. Once we have the book in the
system, we can manipulate that book, and then reflect the book back into
the database for persistence. This is the general idea.

The problem is that there are two subsystems which need knowledge about
books: the database low level module, and the module representing or
manipulating the book. One solution, which is the simple solution, is to
put a book record into a header file, and then refer to the book record in
all modules using it. The problem of this approach is that any user will be
tightly coupled to the books representation, so it is harder to manipulate
the representation of the book.

Any other solution must put the representational knowledge in one module
only. And a little thought shows that it is a bad idea to let the DB module
contain the knowledge. So you construct a "book" module:

-module(book).
-opaque book() :: #book{}.

-spec unmarshal({xml, Doc, Description} | {json, Doc, Description} |
{postgres, ...}) -> {ok, book()} | {error, Reason}.
-spec marshal(Book, xml | json | transit) -> Data.

unmarshal takes a notion of a reification-location, i.e., xml, together
with a document, Doc, and a "Description" which is a symbolic term
describing how to obtain the book from the document. The unmarshaler can
then utilize such a description in order to reify the book from the
document. As a start, you may want to ignore the description and hard-code
the notion of e.g., 'get_title(Doc)', but later on it may be necessary to
generalize this why it can be nice to describe something about the document
you received. A postgres connection would probably just return some
abstract '${ Col => Val }' mapping ColNames to Values in a row. and the
reification process is the obvious one. The point is: *someone* has to
parse the data into a #book{} record, so it may be necessary to have a
generic intermediate format inside the system for doing so.

"unmarshal" is also used to construct a new book for later persistence. One
can imagine adding a variant which is {post, Data, Description} for a
C-in-CRUD creation, with parsing, input validation and all that fluff.

One way of thinking of the description could be path-expressions inside the
Xml document in XPath notation. So the Description is

#{
    title => "a path to the title",
    author => "a path to the author",
    ...
}

In turn, the DB code knows, and provides, something about the *location* of
data inside the document, and the reifier inside 'book' only uses such
location-descriptions to pick out stuff. If the position of the title
suddenly changes in a later Document version, the DB code can simply supply
a different location.

Continuing, what can we do with books? Maybe we have a thumbnail overview
of books. For this, we only need a few entries:

repr(Book, {msgpack, View}) -> msgpack:encode(repr(Book, View));
repr(Book, {json, View}) -> jsx:encode(repr(Book, View));
repr(book#{ title = T, pubDate = PD, smallImageURL = SI },
thumbnail_overview) ->
    #{
        thumbnail = SI,
        title = T,
        pub_date = PD
    }.

Note that a caller can't possible know the internal representation. A
caller gets a "generic" representation[0], which can then be cast in
several forms. Any "update" on the #book{} record has to factor through the
module as well.

Now, a cowboy/webmachine RESTful server would load the book from the db and
call unmarshall on it in the "resource_exists" callback (or perhaps earlier
in the chain in some situations). And once the content-type is negotiated
it would use repr/2 to cast such a generic book record into the desired
content-type for the desired output-view.

[0] In a real system, this will almost invariably get generalized further.
Once you have a couple of these modules, there are commonality which can be
factored out into a more general framework, I'm sure. The key is to avoid
having to come "back" to code and change it whenever some other part of the
system changes. And make sure that changes localize to one or a few
modules. Beware architectures where you have to edit many modules to add a
new "vertical" path in the system. This suggests you should be looking to
"transpose" the code path.

-- 
J.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://erlang.org/pipermail/erlang-questions/attachments/20150712/1ee6729d/attachment.htm>


More information about the erlang-questions mailing list