How to work with deep nesting term

let’s start whit this struct: school → class → student → score
thanks elbrujohalcon’s reply to make it better

-record(student, {
    name :: string(),
    score :: number(),
    more fields...
}).
-type student() :: #student{}.

-record(class, {
    name :: string(),
    student_map :: #{string() := student(),
    more fields...
}).
-type class() :: #class{}}.

-record(school, {
    name :: string(),
    class_map :: #{string() := class(),
    more fields...
}).
-type school() :: #school{}}.

update someone’s score

update_score(School, ClassName, StudentName, Score) ->
    %% suppose args are ok
    %% lookup
    #school{class_map = ClassMap} = School,
    #class{student_map = StudentMap} = maps:get(ClassName, ClassMap),
    Student = maps:get(StudentName, StudentMap),
    %% change score
    Student1 = Student#student{score = Score},
    %% store
    StudentMap1 = StudentMap#{StudentName => Student1},
    Class1 = Class#class{student_map = StudentMap1},
    ClassMap1 = ClassMap#{ClassName => Class1},
    School1 = School#school{class_map =ClassMap1},
    School1.

I think this is classical :upside_down_face:

so, what I want to say is:
1, it seem cost more memory, because of the memory copy in store.(maybe it’s not important?)
2, compare to using ptr(just like, Student.score = Score), more code have to be writed.
3, Should I use erlang:get/1, erlang:put/2 to work like ptr?
4, If I use erlang:get/1, erlang:put/2, I can’t rollback data anymore!

using_ptr(ClassName, StudentName, Score) ->
    put({student, ClassName, StudentName}, Score),
    %% it's not functional! 
    exit(error).

try_using_ptr(ClassName, StudentName, Score) ->
    try using_ptr(ClassName, StudentName, Score)
    catch _:_ -> cant_rollback
    end.

using_term(School, ClassName, StudentName, Score) ->
    exit(error).

try_using_term(School, ClassName, StudentName, Score) ->
    try using_term(School, ClassName, StudentName, Score)
    catch _:_ -> School% rollback easily
    end.

5, I write a kv file like this to solve this problem(lookup, store, update)

lookup([], Struct)  ->
    Struct;
lookup([H|T], Struct) when is_tuple(Struct) ->
    lookup(T, element(H, Struct));
lookup([H|T], Struct) when is_map(Struct) ->
    lookup(T, maps:get(H, Struct)).

6, finally, what’s your solution :blush:

3 Likes

To see if I understood clearly, let’s add some types to your records…

-record(student, {name, score}).
-type student() :: #student{name :: string(), score :: number()}.

-record(class, {name, student_map}).
-type class() :: #class{name :: string(), student_map :: #{string() := student()}}.

-record(school, {name, class_map}).
-type school() :: #school{name :: string(), class_map :: #{string() := class()}}.

Did I get that right? If that’s so, keep reading… Otherwise, forget about the rest of this answer :slight_smile:

I believe that with such a structure you’re duplicating information (basically, the names). If I were you, I would simply drop the records, like this…

-type name() :: string().
-type student() :: #{score := number()}.
-type class() :: #{students := #{name() := student()}.
-type school() :: #{name := name(), classes := #{name() := class()}}.

So that the function would be now…

update_score(School, ClassName, StudentName, Score) ->
  %% extract current value
  #{classes := Classes} = School,
  #{ClassName := Class} = Classes,
  #{students := Students} = Class,
  #{StudentName := Student} = Students,
  %% change score
  Student1 = Student#{score := Score}
  %% store
  School#{classes :=
    Classes#{ClassName :=
      Class#{students :=
        Students#{StudentName := Student1}}}}.

But actually… I would very much recommend you using opaque data structures, instead.
Like this…

-module student.

-opaque t() :: #{score := number()}.
-export_types [t/0].

-export [update_score/2].

-spec update_score(t(), number()) -> t().
update_score(Student, Score) -> Student#{score := Score}.
-module class.

-opaque t() :: #{students := #{string() := student:t()}}.
-export_types [t/0].

-export [update_score/3].

-spec update_score(t(), string(), number()) -> t().
update_score(Class, StudentName, Score) ->
  #{students := Students} = Class,
  #{StudentName := Student} = Students,
  Class#{students := #{StudentName := student:update_score(Student, Score)}}.
-module school.

-opaque t() :: #{classes := #{string() := class:t()}}.
-export_types [t/0].

-export [update_score/4].

-spec update_score(t(), string(), string(), number()) -> t().
update_score(School, ClassName, StudentName, Score) ->
  #{classes := Classes} = School,
  #{ClassName := Class} = Classes,
  School#{classes := #{Classname := student:update_score(Class, StudentName, Score)}}.
5 Likes

thanks for your reply :grinning:

your are right and in this case your structure must be the best choose

I realize that is bad example and I have edited it! :confounded:

if change xxx_map to xxx_list(maybe school is a bad example again?)
then I will using lists:keyxxx to do that
the code size will look like

in work, I can always see code like :rofl:

lookup
lookup
update_value
store
store
2 Likes

Well… With your additional fields, my opaque data structures approach makes even more sense. You can abstract the implementation of each of your entities as much as you want, keep your functions simple, and help dialyzer help you with your types.
In it, if you change the internal structure of… say… student:t() to turn it into a record, you only need to change update_score/2 in that module to adjust it to the new structure.

With proper abstraction levels, the final sequence of actions is still similar… But the code that’s written is modularized and so you just see lookup…update…store.

2 Likes

it’s great desgin and normally we should do like that :blush:

but let’s talk about this situation:
if there is function update_score and update_name
then I will update score and name at the same function:

%% I think that we will often work with this  situation
update_both(...) ->
    update_score(...),
    update_name(...).

using opaque data structures

lookup -> lookup -> update_value + update_value -> store -> store

will become

lookup -> lookup -> update_value ->  store -> store 
+
lookup -> lookup -> update_value ->  store -> store

in erlang, update will copy struct
this make me be careful when using opaque data structures
When I know how erlang work , I will subconscious reduce store operation :upside_down_face:

in other language which can using ptr, just like

lookup -> lookup -> update_value

by the way, another typical example is a huge tuple with erlang:setelement/3
the copy will make me desperate
and array module using nesting to avoid that

It’s hard to make a choice :mask:

2 Likes

In that scenario, I wouldn’t call two setters (update_score/2 and update_name/2).
There are 2 options there:

Random Access

If what you’re working on provides some sort of support for user interaction where random access to individual fields is valid (i.e. you have a table and the user changes any number of fields in a row at once), I would add this functions…

-module student.
…
-type updates() :: #{
  name => string(),
  score => number(),
  registration_year => pos_integer(),
  …
}.
-export_type [updates/0].
…
-export […, update/2].
…
-spec update(t(), updates()) -> t().
update(Student, Updates) -> maps:merge(Student, Updates).
-module class.
…
-export […, update_students/2].
…
-spec update_students(t(), #{student:name() := student:updates()}) -> t().
update_students(Class, Updates) ->
  #{students := Students} = Class,
  NewStudents = maps:fold(update_student/3, Students, Updates),
  Class#{students := NewStudents}.

update_student(StudentName, Updates, Students) ->
  #{StudentName := Student} = Students,
  Students#{StudentName := student:update(Student, Updates)}.

…and you can figure out the code in school.erl as homework :troll:

Specific Access

On the other hand, what you might be after is a specific function that relates to some business-logic of your system, like say… grading an exam, where you have to update multiple fields in your student records (e.g. the score, the date of the last exam, their… something else I can’t think of right now)…

In that scenario, I would just create a function for that piece of business logic. Assuming there is an ODT for exam and it provides exam:t()

-module student.
…
-export […, grade/2].
…
-spec grade(t(), exam:t()) -> t().
grade(Student, Exam) ->
  …update all the fields you need to update at once…
  .
-module class.
…
-export […, grade_students/2].
…
-spec grade_students(t(), exam:t()) -> t().
grade_students(Class, Exam) ->
  #{students := Students} = Class,
  NewStudents =
    maps:map(
      fun(_, Student) -> student:grade(Student, Exam) end, Students),
  Class#{students := NewStudents}.
3 Likes

In this, and most other situations, it’s always wise to remember Joe…

7 Likes

What does this bit of code do?

-type foo : string(), bar : t()
-spec somefun class : t(), exam : t() -> t()

Sorry for the dumb questions I’m learning a bit from the @dominic code. Also welcome to the forums!

2 Likes

thanks for the detailed reply, it’s helpful for everyone :blush:

oh, I got it :grinning:

2 Likes

I hope this doc. can help you :blush:
https://www.erlang.org/doc/reference_manual/typespec.html

2 Likes

Once you have read the link that @dominic just pasted, to understand the crucial part (i.e. -opaque t() :: … . and -export_type [t/0, …].) you should check this article that I wrote a while back…

2 Likes

i would highly recommend to look at https://github.com/jlouis/erl-lenses

3 Likes

oh, OO!
I think I can share my idea which I’m using now(but not in work)
It’s a little bit like OO, but I call it oo(I don’t know how to named it :smiling_face_with_tear:)

let’s begin
I occupy the record’s pos 2 with a strcut to stroe these relation
below extend is not as OO’s extend, it’s just take that pos(not 2) in record
just have a look this example

-record(aaa, {
    oo,
    aaaa
}).
-record(aa, {
    oo,
    aaa :: #aaa{}
}).
-record(a, {
    oo,
    aa :: #aa{}
}).

a extend aa, and aa extend aaa
there’s no other relation between 3 record
with this api

-module(oo).

-type record() :: tuple().

%% it's too long to write the implementation, spec it's ok
%% init oo
-spec new(record()) -> record().
%% extend a 'class'
-spec extend(record(), Pos :: integer(), Father :: record()) -> record().
%% lookup 
-spec lookup(ClassName :: atom(), record()) -> Father :: record().
%% store
-spec store(ClassName :: atom(), record(), Father :: record()) -> record().

work like

-module(aaa).

new() -> oo:new(#aaa{}).

update_aaaa(Value, Record) ->
    AAA = oo:lookup(aaa, Record),
    AAA1 = AAA#aaa{aaaa = Value},
    oo:store(aaa, Record, AAA1).
----------------------------------------------------
-module(aa).

new() -> oo:extend(oo:new(#aa{}), #aa.aaa, aaa:new()).
----------------------------------------------------
-module(a).

new() -> oo:extend(oo:new(#a{}), #a.aa, aa:new()).

update_aaaa() ->
    A = new(),
    aaa:update_aaaa(aaaa, A).

It’s usful when I want to use aaa’s api with an other record bb, just extend!

I my implementation oo look like:
record a: #{aaa => [#a.aa, #aa.aaa], aa => [#a.aa], a => []}
record aa: #{aaa => [#aa.aaa], aa => []}
record aaa: #{aaa => []}

with my kv api, I can do thing like:

kv api work as its function name, If it’s hard to understand just ignore these code

update_class(F, ClassName, KeyList, Record) ->
    %% oo:get_key_list return like [#a.aa, #aa.aaa]
    kv:update(F, oo:get_key_list(Record) ++ KeyList, Record).

add_aaaa(Add) ->
    update_class(fun(AAAA) ->
        kv:update_return(AAAA + Add)
    end, aaa, [#aaa.aaaa], Record).

finally , should I just post a link to my code? beacuse it looks a little bit long :rofl:

2 Likes

the main idea behind lenses is not emulating OO but is about definig composable algorithms and additional combinators
look at http://www.cs.otago.ac.nz/staffpriv/ok/lens.erl for implementation and examples

when constructing lens you are returning function (or object containing function/s) so that you can compose it further.

2 Likes

This answer might be a bit different:

Erlang, as a language, does not support destructive or ephemeral updates. So any update will have to retain persistence of the older term. A lens is a way to handle this conundrum in a functional language where there’s only persistent updates, but lenses tend to work better if they can be guided by a type system; something which Erlang doesn’t have.

However, Erlang-as-an-ecosystem does have support for destructive updates through either ETS or (better) Mnesia, the built-in database system. If you think about the data as rows in tables (ETS or Mnesia), you can store them and do destructive updates to the data. In particular, the task of updating a score becomes far easier.

Essentially, you change your data structure such that it is based on relations rather than nesting. In the relational model, you have all the tooling of Mnesia’s query system at your disposal for retrieving records/rows, and you also have transactions for updating the records.

3 Likes

Just as a note, lenses are very very functional style code, not OO at all.

2 Likes

I’m sorry about that and just like what @OvermindDL1 said

when I see this in the README.md file
The idea of a Lens is to provide an abstraction which is like an accessor/mutator pair in imperative OO languages
I’m too excited and want to share my idea :sweat_smile:

2 Likes