Erlang/OTP Forums

Author Message

<  Erlang  ~  wrapping record with modules

vim
Posted: Mon Jan 05, 2009 3:13 pm Reply with quote
User Joined: 01 Oct 2008 Posts: 19
i have thought of a general way of accessing records by wrapping them with a matching module. e.g. let consider the following record:

Code:
-record(coordinate, {x, y}).


and adding to it the matching module:

Code:
-module(coordinate).
-compile(export_all).

get_x(C) ->
   #coordinate{x=X} = C,
   X.

get_y(C) ->
   #coordinate{y=Y} = C,
   Y.

add(C1, C2) ->
   X = get_x(C1) + get_x(C2),
   Y = get_y(C1) + get_y(C2),
   #coordinate{x=X, y=Y}.


does it make sense, what are the cons/pros in your opinion?
View user's profile Send private message
Mazen
Posted: Mon Jan 05, 2009 5:31 pm Reply with quote
User Joined: 20 Jul 2006 Posts: 164 Location: London
There are two really good benefits of doing this;

1) You can use the record as any other type to represent the x and y. From another module's perspective this is just an abstract data type that the user shouldn't look into (Like for example the dict module etc.). This makes it easy to maintain.

2) Maintenance and update issues are less likely to become a problem. As long as the user use the wrapper module then nothing else needs to be updated other then the wrapper module (assuming backwards compatible API).

This is a good way to create a library interface...

/Mazen
View user's profile Send private message
vim
Posted: Mon Jan 05, 2009 8:01 pm Reply with quote
User Joined: 01 Oct 2008 Posts: 19
i read this opinion in favor of using more simple tuples rather than using records in function interfaces. what do you think?

and referring to my original question, is there a fine line of abusing records, or is it the best practice way to connect modules? i know there is no yes/no answer but i would like to hear the arguments in favor or against each approach Smile

cheers,

p.s. thank you mazen for your numerous replies
View user's profile Send private message
dbudworth
Posted: Mon Jan 05, 2009 8:39 pm Reply with quote
Joined: 05 Jan 2009 Posts: 2
In general it's a good approach that allows you to change the definition later. But I'd make a small change to this.

Code:

get_x(C) ->
   #coordinate{x=X} = C,
   X.


This will fail if "C" is not a #coordinate record. And the failure will be in your module.

Alternatively, that could be written as:
Code:

get_x(#coordinate{x=X}) -> X.


the difference? This version will fail at the call site as an unmatched function. Meaning, there is no
get_x("I like puppies")

So the consumer of your module is told straight away that they are at fault for passing the wrong structure. Otherwise, they have to look in to your function to see where the bad match is and why it's a problem.

Make sense?

example from shell:
Code:

1> F1 = fun(C) -> {coordinate,X,_Y} = C, X end.
#Fun<erl_eval.6.13229925>
2> F2 = fun({coordinate,X,_Y}) -> X end.
#Fun<erl_eval.6.13229925>
3> F1({coordinate,5,3}).
5
4> F2({coordinate,5,3}).
5
5> F2("I like puppies").
** exception error: no function clause matching erl_eval:'-inside-an-interpreted-fun-'("I like puppies")
6> F1("I like puppies").
** exception error: no match of right hand side value "I like puppies"


In this trivial example, both are obvious. But in anything even slightly more complicated, the "F1" version will be harder to track down.

-David

p.s. {coordinate,X,Y} is what the record syntax is really generating. It's just easier to do that than use a real record from the shell.
View user's profile Send private message
Mazen
Posted: Tue Jan 06, 2009 2:37 am Reply with quote
User Joined: 20 Jul 2006 Posts: 164 Location: London
I read the article and I completely agree with the author. However I don't think that records are useless I just think that you should be careful when to use them, and there are a few things he is missing though.

In the article the author mentions an interface area/1 which calculates the area, fine, but area of what? Obviously in the article he mentions of a circle but it is not clear from the name, however we could most probably argue that it would never be called just area in reality, it would probably be called more like circle_area/1. In my oppinion this is where we start running into logical contradictions.

I have seen it many times that people use function names in a higher abstraction level like in this case area but then completely kills the abstraction by introducing a tag in the input data. It's like going in to a restaurant and first tell them that you want food, then they ask you what kind of food and you tell them a steak or whatever, instead of just saying "I want the steak".

I.e. circle_area/1 is better then area/1 that checks for which type you want to calculate the area for. (I am aware that the author probably wouldn't just call it area (unless he was doing a circle handling library Very Happy) but I'm just using it as an example)

Now assuming that this makes sense then it also makes sense to skip the records and tuples in APIs all together. There shouldn't be any data structures used in an interface unless the data type is abstract! 99% of the times where I have created/discovered bugs concerning interfaces and data structures (like tuples of various sorts) in both min and other's code it has been because the data structure is expected to be "known" outside of the module, this clearly doesn't make sense.

I actually, not very long ago discussed this with a collegue of mine and we had a very long discussion about how to use records. We eventually agreed that if you create a record, keep it in your application, don't (or at least try as much as you can) not to expose it.

A very good example of how it can go wrong is when using appmods in Yaws. A year and a half back I was working with the predecessor to ErlangWeb. At that time it was only running on Yaws. ErlangWeb would have an appmod registered in Yaws, then Yaws would call a callback in a module with 1 argument which was a big record with a bunch of information about the request coming in. This record was manipulated and a result was spit out, easy clean and neat... Until someone used a newer version of yaws OR an incompatible new version. ErlangWeb was including a header file with a record definition and it was using it everywhere (almost). This meant that if the record was changed, not only would we have to recompile the code but it sometimes would completely break. This made ErlangWeb completely dependent on that version of Yaws (disclaimer: I don't know how it is now though but I'm sure ErlangWeb doesn't have this anymore since we have discussed this so much in our office).

In my view the best solution would be to provide the request as an abstract data type and have an api in Yaws which extracted and manipulated this data type. This would completely solve the problem. As long as I don't look inside the data type and use it in the API then I can argue that I should ALWAYS be safe.

These are the two aspects I see when working with interfaces. If your data is known; try to stick to basic data types in your api. If you are provided with an abstract data type then choose because it doesn't matter, you are not putting any headache on the users. If you need to add parameters to complement the function then stick to basic data types if you can.


Wow... just realized the time over here... I better go to bed Very Happy

/M
View user's profile Send private message
vim
Posted: Tue Jan 06, 2009 10:20 pm Reply with quote
User Joined: 01 Oct 2008 Posts: 19
the thing is i wrote down a module which process a record, which is passed from other modules, which have prepared this record.

lets say: #troop{type, amount, location}

it was all neat and nice, till i needed to add a new field to that record. this how i found the big disadvantage of using records: you can't rely on pattern matching.

after i added the new field, e.g. #troop{type, amount, location, owner}, i encountered several bugs, which crippled because i forgot to fill the new field's content on all the functions that prepared or used it. the compiler didn't catch that of course, just the tests. so it took me considerable amount of time to nail all those missing places where i forgot to add the field.

now, lets say i was using a small tuple instead, getting into the same scenario, where i forgot to add it on few functions, the pattern matching would have failed, producing a compiler errors on all those spots. this can be a very big advantage. more strict pattern matching over the record flexibility seems more important in light of this case.

the tuple would be similar to the record, of course: {troop, Type, Amount, Location}

the tuple approach down side, which i can spot, is it can't grow too long. but then, i can break it apart to smaller tuples, like:
{troop, Type, Amount, Location, Params}
Params = {params, Color, Speed, Level}
etc.

Rolling Eyes
View user's profile Send private message
Mazen
Posted: Tue Jan 06, 2009 11:51 pm Reply with quote
User Joined: 20 Jul 2006 Posts: 164 Location: London
{troop, Type, Amount, Location} _is_ a troop record... the "#troop{ ... }" part is just syntetic sugar. The record is made into a tuple when compiled. If you write:

Code:

-module(foo).
...
bar(#troop{ type = Type }) -> Type.


you will always be safe against changes in the record as long as you recompile the foo module when you update your definition (Erlang does this automatically though).
View user's profile Send private message
vim
Posted: Wed Jan 07, 2009 6:55 am Reply with quote
User Joined: 01 Oct 2008 Posts: 19
what i start to think is i need to use a function call to 'help' me create new record instances. so, if i take the idea i started this thread with, and add a new() function (so every one wish a new record use the new() instead of creating it by himself), i can enforce filling up new fields.

e.g., for #troop{type, amount, location}:

Code:
-module(troop).
-export([new/3]).
new(Type, Amount, Location) ->
   #troop{
      type = Type,
      amount = Amount,
      location = Location
   }.


when adding new field owner, i just change the arity of the new function to new/4 - forcing every one using to to change the code, and fill in the missing field.
View user's profile Send private message
rvirding
Posted: Wed Jan 07, 2009 7:48 pm Reply with quote
User Joined: 30 Aug 2006 Posts: 452 Location: Stockholm, Sweden
I would definitely use records in cases like this for many reasons:

- As has already pointed out it is much easier to add/remove fields from a record and have the compiler check/make it correct.

- With a record there is less chance that you will get a stray tuple as input which just happens to fit.

- When debugging the tag will help you see what type of data this is, this is not safe mind you but at least a help.

Also the added cost is minimal.

Records are static, if you want a really dynamic "thing" then you should use a dict or tree which would allow you to add/remove fields on the fly. Though the cost is higher and potential confusion greater.

Robert
View user's profile Send private message Visit poster's website MSN Messenger

Display posts from previous:  

All times are GMT
Page 1 of 1
This forum is locked: you cannot post, reply to, or edit topics.

Jump to:  

You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You cannot download files in this forum