Private 'getters' for structs

1,070 views
Skip to first unread message

Peter Saxton

unread,
Sep 28, 2015, 8:31:17 AM9/28/15
to elixir-lang-talk
When using structs I have been told it is good practise to write module functions to access data in a struct. The classic reason why being for users and names

defmodule User do
  defstruct name: nil

  def name(user) do
    user.name
  end
end

The reason for the name function is so that if the data structure changes (eg if it saves first and family name) then the name function can be changed and all calls to it will still work.

I know this now. however it is still very tempted to make quick calls into structs that are passed around.

For that reason I was wondering what the possibility/implication of making the call on a struct private would be. %User{name: "dave"}.name would raise an error/warning if called outside the user module.

José Valim

unread,
Sep 28, 2015, 8:42:58 AM9/28/15
to elixir-l...@googlegroups.com
When using structs I have been told it is good practise to write module functions to access data in a struct

Have been told by? :)

Structs are only data, if you hide the data, then they are nothing. Sure you can put some of the access behind functions but there is no purpose on doing that as a rule.

Peter Saxton

unread,
Sep 28, 2015, 8:50:35 AM9/28/15
to elixir-lang-talk, jose....@plataformatec.com.br
So I cant remember who by. However I remember the logic and it made sense

When my user gets upgraded to

defmodule User do
  defstruct first_name: nil, last_name: nil

  def name(user) do
    "#{user.first_name} #{user.last_name}"
  end
end

Then calls of "user.name" are broken but calls of "User.name user" are not broken. Essentially direct access to the data is outside of the interface defined by a module.
Its protection rather than hiding. I understand that protection is not really the best word either as being immutable they dont really need protection.

Ultimately I think that the use of "user.name" would lead to a situation where people will want to define there own getters and you end up with JS style defining getters.

Peter Saxton

unread,
Sep 28, 2015, 8:51:35 AM9/28/15
to elixir-lang-talk, jose....@plataformatec.com.br
Apologies for the use of interface and getter in particular. I'm not sure what terms are better but I think they are probably too OOP 

Onorio Catenacci

unread,
Sep 28, 2015, 12:16:53 PM9/28/15
to elixir-lang-talk
I may be the guilty party José but if that's the case I think Peter misunderstood what I was trying to tell him. We were discussing some stuff on the Slack channel (if memory serves) and I was discussing the notion of "information hiding".  Information hiding long predated the OO idea of encapsulation but it's along the same lines.  That is, don't expose details about your implementation that you don't need to expose.  But as I say, if I were the one that mentioned that to Peter (and I think I may have been) then I think he misunderstood my point. 

I'm not encouraging anyone to write their code with getters; I'm encouraging developers to err to the side of hiding details rather than revealing them.  A getter by definition is revealing at least two things:  1.) That there's a data member and 2.) The type of the data member (in statically typed languages).  Both of these are often details that don't need to be exposed--especially in a functional language where one shouldn't think of data as being tied to code.  As far as I'm concerned having getters and setters _everywhere_ and for most data members in a class is an anti-pattern that Java and C# have saddled OO with. While getters/setters seem to help encapsulation they, in fact, erode it. I'm also pretty sure that I added that part about anti-patterns to the discussion on Slack but perhaps that was missed out. :)

I know it's tough making the transition from OO to FP but getters/setters everywhere is a bad idea in plain OO for the reason I laid out above.  I mean even if someone doesn't care to transition to FP getters and setters everywhere is a bad idea in plain old Java and C#. There's really nothing to be gained by carrying that anti-pattern forward into FP. 

As I say, apologies if I caused any misunderstanding with that discussion on Slack (although I'm not sure it was me :) ).


--
Onorio

Ed W

unread,
Sep 28, 2015, 12:40:46 PM9/28/15
to elixir-l...@googlegroups.com
I'm one of those who has a hard time letting go of encapsulation of functions and data (I don't tend to write in an OO style either...)

For example, it won't be long before you want to enforce your data always has some properties, eg in our %User{name: "Fred Bloggs"} situation we might want to enforce he a) has a name and b) the name starts with a capital letter (and not a number). I use my person object quite extensively around my project and feel unsure where to put my code to avoid duplication?

Also, originally there were no restrictions on naming strategy, the requirements above arrived later on and it was (hypothetically) annoying having to refactor the whole code to force everyone to use the "name" helper function.  Next week I will have a surprise new requirement put on me: we will discover that some legacy component can't handle UTF-8 and so all names much be ASCII only. Again having established the use of a helper function will aid greatly when this needs to be implemented

... discuss ...

Ed W
--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/0150df87-17bb-4ede-a503-2b66c16cab27%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

José Valim

unread,
Sep 28, 2015, 12:48:38 PM9/28/15
to elixir-l...@googlegroups.com
For example, it won't be long before you want to enforce your data always has some properties, eg in our %User{name: "Fred Bloggs"} situation we might want to enforce he a) has a name and b) the name starts with a capital letter (and not a number). I use my person object quite extensively around my project and feel unsure where to put my code to avoid duplication?

Also, originally there were no restrictions on naming strategy, the requirements above arrived later on and it was (hypothetically) annoying having to refactor the whole code to force everyone to use the "name" helper function.  Next week I will have a surprise new requirement put on me: we will discover that some legacy component can't handle UTF-8 and so all names much be ASCII only. Again having established the use of a helper function will aid greatly when this needs to be implemented

Data should be validated at the boundaries. You want to be confident about your data. If you look at a struct field and you think "I am not really sure what is here" then you have a problem.

How to solve this problem? Validate data at the boundaries. If you are using Phoenix, this may mean before putting the data in the database, using Ecto changesets. If the data in your database is "dirty", which is already a bad signal on its own, normalize the data after you get it from the database:

    users = User |> Repo.all |> User.normalize_names

Then, every time you access "user.name", you are sure it is exactly what you need. What you don't want is to pass data around that you are not exactly sure how it looks like. You also don't want to hide the maybe expensive process of normalizing data in ad-hoc calls to User.name/1.


Ed W

unread,
Sep 28, 2015, 1:04:44 PM9/28/15
to elixir-l...@googlegroups.com
I agree in principle with everything you say above, but I worry about the situation where I want to either carry the data around for a while before persisting it, or equivalently "fail fast" and report a validation error earlier in the process rather than waiting for later when we persist.  This might be because I'm trying to architect my whole solution "incorrectly" though...

We are working on analogy's so forgive if this gets a bit strained, but I'm trying to construct some situation we want to update the name field, avoid saving the object yet, pass it to some other process for display, etc, perhaps a few more edits and it will be some way down the line before we bump the Ecto validations.

(OK, so try and be concrete: we ask the user to enter their name, we then show them what they wrote and asked them to confirm, they say yes and we THEN try and create a new persistent object in our storage. However, if the user entered an impossible name we should fail *before* the confirm step and get them to try again (or fixup according to our rules), not wait until the persist step.  Also, where should the fixup step code live, eg if business requirements say fixup silently, eg converting unicode character to ascii, then do we centralise this in a setter or in the Ecto changeset?)


My old school thinking is panicking and wanting to get the data fixed up ASAP, so my intuition is to enforce the validation in some "setter".  Please consider that I'm somewhat handicapped because *you* might be several steps ahead of me and I'm still staring at my feet and can't yet see your solution, eg perhaps in your design you will re-architect such that
- a bunch of fields get updated in the map (rather than individually)
- call the changeset validation function on the whole map
- do something with the result of the validation including "fail fast" or otherwise aborting the whole operation?

So, if it helps you understand why I "don't get it", my intuition is to update little bits of the data structure one by one, validating one by one as we go.  Is this actually the bit you would tell me is "wrong"?

If the answer is to shift thinking to thinking about larger changesets and validating the whole changeset before anyone gets to see it again, then probably this is the bit of thinking I'm missing?  (And hence why I'm reaching to put the validation/fixup into the "setter" functions)

Can someone hit me with the clue stick?

Thanks

Ed W

José Valim

unread,
Sep 28, 2015, 1:28:17 PM9/28/15
to elixir-l...@googlegroups.com
(OK, so try and be concrete: we ask the user to enter their name, we then show them what they wrote and asked them to confirm, they say yes and we THEN try and create a new persistent object in our storage. However, if the user entered an impossible name we should fail *before* the confirm step and get them to try again (or fixup according to our rules), not wait until the persist step.  Also, where should the fixup step code live, eg if business requirements say fixup silently, eg converting unicode character to ascii, then do we centralise this in a setter or in the Ecto changeset?)

There is no setter. You also can't mutate the User map in place. So you have no other option than do it elsewhere. Even if you modify the User directly, the original User is still available because the data structures are immutable.

Take a look at Ecto.Changeset.cast/4 documentation. What will happen is roughly this:

input: You are going to call the Ecto.Changeset.cast/4 with the current model version, with the external data (that may be invalid), the required fields for that change and all optional fields

output: an Ecto changeset. It tells exactly which fields have changed, if any of them is invalid and so on

Once you have the changeset, you have a map with all changes that will be sent to the database if everything you have so far is valid. You can then further validate and manipulate the changeset. For example, you can write:

cast(model, params, ~w(name address), ~w())
|> validate_length(:name, min: 3)
|> normalize_name()

And normalize_name would look like this:

case changeset do
  # If the changeset is valid and the name changed, normalize it
  %Ecto.Changeset{valid: true, changes: %{name: name}} ->
    put_change(changeset, :name, convert_utf8_to_ascii(name))
  # Otherwise, do nothing
  _ ->
    changeset
end

Everything is in the changeset. Your User model did not get "dirty". All the changes, validations, errors have been explicitly collected so you can choose how to proceed accordingly.

Peter Saxton

unread,
Sep 28, 2015, 7:14:10 PM9/28/15
to elixir-lang-talk
A lot of good stuff here.

First Onorio you might be one of the guilty parties but I have discussed with others and managed to come up with reasons to back it up.

Second can I just check, that we all mean the same thing by getter?
I think 'user.name' is using a getter where the '.name' is a getter on the data object
I think 'User.name user' is a module function.
Is that the same as what everyone else is talking about.

Third I agree with validating data at program boundaries. Though I think this is true without the getters issue and without a differentiation between OOP/FP
I wrote bit about value objects in Ruby and a follow up article on protecting the application boundaries.
One fast rule I had in Ruby that has so far held in elixir is never have an invalid representation of any concept within the program core.
It's ruby but immutable so might be of interest.
http://insights.workshop14.io/2015/07/15/value-objects-in-ruby.html
http://insights.workshop14.io/2015/07/23/application-border-control-with-ruby-form-objects.html

Fourth. My suggestion was to use getters on structs no where outside of the module that defines the struct. (there were a few comments about getters everywhere hence I thought worth clarifying)
I also do not use the basic struct creation outside of a few functions again in the same module

user = User.create_representation_by_some_means

name = User.name(user)
# Instead of
name = user.name

the user variable has a representation of a user. I think that knowing that the representation is a struct is too much knowledge even without any consideration of validation or changing the value of a name.

We might want the create function to return a pid so we get the users current screen name which they can change freely
We might want the create function to return an id and we only look up the name it a list of names when it is asked for.

Richard B

unread,
Sep 28, 2015, 10:09:58 PM9/28/15
to elixir-lang-talk
I wouldn't think of user.name any more of a getter as I would think of user[name] or Map.get(user, :name). I think it's the dot notation that might be causing confusion?

Luper Rouch

unread,
Sep 29, 2015, 2:35:35 AM9/29/15
to elixir-lang-talk
Is it extendable to all "methods" in structs, e.g. should we avoid putting any code in a struct and define it elsewhere?

Here is for example something I wrote to track runs variance in a benchmark tool:
    
    defmodule State do
      defstruct [
        requests_count: 0,
        successes_count: 0,
        errors_count: 0,
        duration: %OnlineVariance{},
      ]

      def add_result(state, _success = true, duration) do
        duration = OnlineVariance.update(state.duration, duration)
        %{state |
          duration: duration,
          requests_count: state.requests_count + 1,
          successes_count: state.successes_count + 1,
        }
      end
      def add_result(state, _success = false, _duration) do
        %{state |
          requests_count: state.requests_count + 1,
          errors_count: state.errors_count + 1,
        }
      end
    end

Should this kind of code be avoided or does it seem OK to you?

Thanks,
Luper

On Tue, Sep 29, 2015 at 4:10 AM Richard B <rbis...@gmail.com> wrote:
I wouldn't think of user.name any more of a getter as I would think of user[name] or Map.get(user, :name). I think it's the dot notation that might be causing confusion?

--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.

Peter Saxton

unread,
Sep 29, 2015, 3:07:07 AM9/29/15
to elixir-lang-talk
All of that code seams ok to me. The reason being you have contained all knowledge that a struct exists within the structs module


defmodule User do
  defstruct name: nil

  # Using user.name in this module makes perfect sense.
  def print_name(user) do
    IO.puts(user.name)
  end
end

defmodule Greeter do
  # This function knows some implementations about the user representation outside of the user module and so is in a small way not respecting the user module interface
  def nosy_greet(user) do
    IO.puts "Hello #{user.name}"
  end

  # This function uses the interface and only knows that the user represents and has no idea how it is implemented.
  def polite_greet(user) do
    IO.puts "Hello #{User.name(user)}"
  end
end

The key point is not that a User.name/1 function always needs to be written, it would not be written if the raw name was never used outside the user module.
The reason nosy greet seams unagreeable is that the knowledge of the form of the user representation( ie that it is a struct) has left the User module

José Valim

unread,
Sep 29, 2015, 3:19:59 AM9/29/15
to elixir-l...@googlegroups.com

should we avoid putting any code in a struct and define it elsewhere?

No, it is fine to put code in the same module the struct has been defined.




José Valim
Skype: jv.ptec
Founder and Director of R&D

Onorio Catenacci

unread,
Sep 29, 2015, 8:28:45 AM9/29/15
to elixir-lang-talk
At this point I think some of this is just a question of terminology.  The thing is the term "getter" (rightly or wrongly) has a strong association to the OO paradigm.  While FP and OO are not orthogonal concepts, the way most OO is actually practiced, i. e. with imperative code constructs, they might as well be mutually exclusive.  So, you would be doing yourself a great favor Peter to jettison the concept of a "getter" because, rightly or wrongly, when most of us hear that word we hear OO and when we hear OO we assume "not FP".  

It's perfectly ok to write a function to examine some member of a data structure (generic sense) and it's even ok to call it a "getter". However by calling it a "getter"  you're only muddying the water because when people hear "getter" they think OO. You're far better off, if you plan to stay with FP, to let go of at least the terminology of OO. Not for technical reasons but for practical ones.

Part of the point I was trying to make about information hiding is that, if possible, you're even better not to have a function to expose details about a data structure, i. e. no getter if possible. Every time you expose details, you couple the data structure more tightly to other code.

--
Onorio

Ed W

unread,
Sep 30, 2015, 4:55:10 AM9/30/15
to elixir-l...@googlegroups.com
On 29/09/2015 13:28, Onorio Catenacci wrote:
> At this point I think some of this is just a question of terminology.
> The thing is the term "getter" (rightly or wrongly) has a strong
> association to the OO paradigm. While FP and OO are not orthogonal
> concepts, the way most OO is actually practiced, i. e. with imperative
> code constructs, they might as well be mutually exclusive. So, you
> would be doing yourself a great favor Peter to jettison the concept of
> a "getter" because, rightly or wrongly, when most of us hear that word
> we hear OO and when we hear OO we assume "not FP".
>
> It's perfectly ok to write a function to examine some member of a data
> structure (generic sense) and it's even ok to call it a "getter".
> However by calling it a "getter" you're only muddying the water
> because when people hear "getter" they think OO. You're far better
> off, if you plan to stay with FP, to let go of at least the
> terminology of OO. Not for technical reasons but for practical ones.
>
> Part of the point I was trying to make about information hiding is
> that, if possible, you're even better not to have a function to expose
> details about a data structure, i. e. no getter if possible. Every
> time you expose details, you couple the data structure more tightly to
> other code.
>
> --
> Onorio
>

Thanks

Does anyone have a suggestion on books which cover "functional thinking"
in the sense above. To be specific this is an architecture question,
not a coding question. So assume I can write basic haskell/elixir, my
problem is I'm not "getting" how to think in the way you describe above
to compose (larger) programs

Cheers

Ed W

Booker Bense

unread,
Oct 1, 2015, 10:46:27 AM10/1/15
to elixir-lang-talk


On Wednesday, September 30, 2015 at 1:55:10 AM UTC-7, Ed Wildgoose wrote:

Does anyone have a suggestion on books which cover "functional thinking"
in the sense above.  To be specific this is an architecture question,
not a coding question.  So assume I can write basic haskell/elixir, my
problem is I'm not "getting" how to think in the way you describe above
to compose (larger) programs. 

You might find this talk really useful, there are some books listed there that focus
on designing protocols. 


The suggested approach is very similar to one that comes up in some Ruby design books,
"Think about the messages in your system first, not modeling objects." 

Since the BEAM can be thought of as a very efficient client/server network with microservices, if you 
design around messages between services ( or Actors) that design will map naturally to
BEAM processes.  

Plus Torben is a very entertaining speaker. 

- Booker c. Bense
Reply all
Reply to author
Forward
0 new messages