Elixir best practice for setting two values on a condition

273 views
Skip to first unread message

Brian Bugh

unread,
Aug 23, 2016, 8:51:22 AM8/23/16
to elixir-lang-talk
For fun and profit, I am writing a Brainf**k interpreter in Elixir.

In one particular case, a data pointer should be decremented, and if it's less than 0, it should be set to 0 and the top of the data stack should be popped off.

I assumed that I should write something like this (which passes my :

dataptr = dataptr - 1

if dataptr < 0 do
  dataptr
= 0
  data
= tl data
end

but I get a compiler warning when I do this. It suggested that I use the assignment form of if, which is fine, but I can't find an elegant way to write the code now. This is what I came up with, which seems uglier and unnecessarily verbose for future readers. The first form above is much more readable.

[dataptr | data] = if dataptr < 0 do
 
[0 | tl data]
else
 
[dataptr | data]
end

Is there a better Elixir-way to write this?

Brian Bugh

unread,
Aug 23, 2016, 9:08:16 AM8/23/16
to elixir-lang-talk
Here's another ugly one I came up with (with the complete function for reference) that is even worse. 

def command("<", state = %{data: data, dataptr: dataptr}) when is_list(data) and is_integer(dataptr) and dataptr >= 0 do

  dataptr
= dataptr - 1


  state
= case state do
   
%{dataptr: dataptr} when dataptr < 0 -> %{state | dataptr: 0, data: tl(data) }
    _
-> state
 
end

 
{:ok, %{state | dataptr: dataptr, data: data}}
end

Usually when I get stuck like this it means I'm overthinking something. 

Any suggestions?

Torben Hoffmann

unread,
Aug 23, 2016, 9:24:51 AM8/23/16
to elixir-l...@googlegroups.com
Hi Brian,

How about this?

  def command("<", state= %{dataprt: 0}) do
    {:ok, %{state | data: tl(data)}}
  end

  def command("<", state) do
    {:ok, %{state | dataprt: state.dataptr - 1}}
  end

By using the pattern matching in the function clauses you get code that reads like your description of the problem.
I.e., if the dataptr is zero pop the data stack otherwise decrement the dataptr.

Cheers,
Torben

--
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-talk+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/bfcb8bdf-aede-4955-ac50-a6ac6dde3e5a%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Brian Bugh

unread,
Aug 23, 2016, 9:55:29 AM8/23/16
to elixir-lang-talk
It turns out that I think my assumption about how the BF interpreter works is wrong, but I want to continue this discussion because while my interpreter may be bad, I still don't know the answer to my Elixir question. :) 

Torben - thanks for the idea! I did try it with function pattern matching. However, the actual command is dataptr - 1 , so that needs to run first. Then if the dataptr is less than 0, the other check runs.  The way you've suggested it in the first command assumes that the < command has already been run. :) 

Here's what I tried with pattern matching, which is even less comprehensible than that simple little if statement at the top.

  def command("<", state = %{data: data, dataptr: dataptr}) when is_list(data) and is_integer(dataptr) and dataptr >= 0 do

    do_trim
(%{state | dataptr: dataptr - 1})
 
end

 
def do_trim(state = %{data: data, dataptr: dataptr}) when dataptr < 0 do
   
%{state | dataptr: 0, data: tl(data) }
 
end

 
def do_trim(state = %{data: data, dataptr: dataptr}), do: state

Having been a programmer for 23 years, I'm a huge fan of writing code that my tired, burned out, working-on-the-weekend-because-release-date-is-Monday future self won't have to think about too hard to understand. 

Nothing I've come up with so far is as clear as that simple little if statement.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.

OvermindDL1

unread,
Aug 23, 2016, 10:06:50 AM8/23/16
to elixir-lang-talk
If you want it inline then tuples are the traditional 'return multiple values from an expression (like if)' thing, and yes, this is the functionally traditional way of doing it.

{dataptr, data} = if dataptr < 0 do
  
{0, tl data}
else
  
{dataptr, data}
end


Moving it out into another function like the other posts is easy too, but honestly no point considering how short the requirement is and that it is likely not re-usable (if it is re-usable elsewhere, definitely move it out).

In general, keep functions simple, I try to keep them at no more than a few lines (I have many cases that I go over though), there is no harm on having lots and lots of defp functions as they get compiled together and optimized.

OvermindDL1

unread,
Aug 23, 2016, 10:08:20 AM8/23/16
to elixir-lang-talk
This one really should be broken up for readability of it.  Generally if your lines get 'that' long, more functions are better.  Remember that defp functions have no real cost.

Torben Hoffmann

unread,
Aug 23, 2016, 5:24:48 PM8/23/16
to elixir-l...@googlegroups.com
Hi Brian,

Looking at what your code does and what I suggested I don't see a difference.

Your command/2 will subtract 1 from dataprt and if dataptr < 0 then you will reset dataptr to 0 and pop data.

My solution took the liberty of saying "if dataptr=0 (before you subtract) we have to pop data otherwise we subtract one from dataprt and that is it".
That should do the same as your code, because if dataptr=0 when you run your command/2 function you will end up with dataptr=-1 before calling do_trim, which will reset it to 0 and pop data.

Reading the Brianfuck page on Wikipedia seems to suggest that the < command should only decrement the dataptr, so I am not sure what the pop you are doing with tl is good for. But then again, Brainfuck is not supposed to be easy to understand...

I actually can't believe that I am going to write the following... ;-)

I would consider using two lists to hold the array of cells. Say, left and right fields in your state.
Initially we would have left=[] and right= "a bunch of zeros".
Then we get something like this

  def command("<", state = %{left: []}) do
    state # we silently do nothing when moving too far
  end
  
  def command("<", state = %{left: [h|t]} do
    %{state | left: t, right: [h | state.right]}
  end

  # we always leave one element in right as that is the end of our world
  def command(">", state = %{right: [_]} do
    state
  end

  def command(">", state = %{right: [h|t]}) do
    %{state | left: [h | state.left], right: t}
  end

  def command("+", state = %{right: [h|t]}) do
    %{state | right: [ h+1 | t ]} # should probably do some overflow handling if we want to keep having a cell as a byte
  end
  ...
  def command("[", state = %{right: [0|t]}) do
    state
  end

  def command("[", state) do
    # this is where the fun starts, now we need to enter a mode where we loop the code 
    # until we have balanced out [ and ].
    ...
  end
    
And already here I can see that using command with just two arguments will probably be hurtful in the long run.
Perhaps the function should be called interpret and take a list of chars as the first argument and simply peel off the first element.
When we reach a loop we have to start accumulating the commands in case the loop has to run more than once.
But if we call a loop function that executes commands and return the collected loop body as well as an updated state, then we simply have to check the value at the data pointer when the loop has been collected. If it is zero we throw away the collected program, if not we re-insert the collected program in front of the rest of the program and call interpret again.

Now I am almost tempted to go write the full interpreter, but luckily for me it is late at my end right now, so I'll have to see if I am up for it tomorrow!!

Cheers,
Torben


To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-talk+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/743c6c3e-0def-4cf6-8dee-2338a19c0ef6%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Brian Bugh

unread,
Aug 23, 2016, 7:31:11 PM8/23/16
to elixir-l...@googlegroups.com
Ah you're right! Very clever. That's awesome, thanks for explaining. 

The implementation I posted was doing the < and > commands backwards, because I was thinking about it being cheaper to prepend to lists instead of appending, but there's no way around it. 

The Wikipedia article about BF isn't very helpful. I've written an interpreter successfully in Ruby, but I'm doing a clean room implementation of it in Elixir so I can't "cheat". 

The < and > commands assume you have a data pointer list of infinite size, 0 based. Something like Array.new(n, 0) in Ruby. So if you run a BF program like <<<<<<+, you'd end up with a data structure of [1, 0, 0, 0, 0, 0], hence the required list manipulation. 

- Brian
You received this message because you are subscribed to a topic in the Google Groups "elixir-lang-talk" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elixir-lang-talk/_BPxgTc4VGM/unsubscribe.
To unsubscribe from this group and all its topics, 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/CABf3pCmnvMWt3xqvOC3Yim06XpLoSRkqzYqjPUoSp1HurGQbuw%40mail.gmail.com.

OvermindDL1

unread,
Aug 24, 2016, 11:00:29 AM8/24/16
to elixir-lang-talk
Instead of holding 'data' and a 'dataptr' you could make the `data` data structure a zipper where the location of the zipper is the ptr.  Would be very efficient.


There might even be a library or two on it on hex.pm.  :-)

Brian Bugh

unread,
Aug 29, 2016, 7:49:10 AM8/29/16
to elixir-lang-talk
Thanks for the great idea! I forgot about zippers. This was exactly what I needed. There was a zipper tree library on hex but it was overkill for what I needed, so I made my own: https://hex.pm/packages/zipper_list  

OvermindDL1

unread,
Aug 29, 2016, 10:45:55 AM8/29/16
to elixir-lang-talk
Awesome!  Been wanting a good Elixir Zipper library, the others I'd found were for Erlang and had different usages.  :-)

Easy to do it yourself though of course, but libraries also help document usage instead of just using a {[],[]} as a zipper.  :-)

Ben Wilson

unread,
Aug 29, 2016, 6:28:46 PM8/29/16
to elixir-lang-talk
Can you elaborate on what you'd like out of a zipper library not found in Stream/Enum?

OvermindDL1

unread,
Aug 29, 2016, 6:57:08 PM8/29/16
to elixir-lang-talk
Entirely different concepts.  :-)

'zipping' a list means combining multiple lists into a single list of tuple/lists, which is what Enum/Stream does.

A Zipper data structure is essentially a tuple of two lists, for example:
{[], [1, 2, 3, 4]}
-> Move right
-> {[1], [2, 3, 4]}
-> Move right
-> {[2, 1], [3, 4]}
-> Move right
-> {[3, 2, 1], [4]}
-> Move left
-> {[2, 1], [3, 4]}

It is basically a way of iterating backwards and forward through a list (which is what a BF interpreter does to access its memory) in a very efficient, functional, immutable method.  The left side holds the reversed 'data' to the left of your 'cursor', and the right holds the 'data' to the right of your 'cursor', where the cursor is just the current position along the entire structure.

Zippers can also be more complicated as well, allowing you to efficiently and immutable walk along an entire tree structure back and forth efficiently.

Ben Wilson

unread,
Aug 29, 2016, 7:54:28 PM8/29/16
to elixir-lang-talk
Sounds at least a bit like http://erlang.org/doc/man/queue.html

José Valim

unread,
Aug 29, 2016, 8:00:28 PM8/29/16
to elixir-l...@googlegroups.com
Queues do not give control over that structure though. Fred wrote a good article on zippers: http://ferd.ca/yet-another-article-on-zippers.html
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-talk+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/30f5bbd0-b3d3-4309-bf35-6707cf3d723e%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.


--


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

Reply all
Reply to author
Forward
0 new messages