Fl4m3Ph03n1x
How to use streams with char lists?
Background
I have the following code, which takes a string, converts it to a charlist and then maps over it.
defmodule RotationalCipher do
@alphabet_size 26
defguard is_lower?( char ) when char in ?a..?z
defguard is_upper?( char ) when char in ?A..?Z
def rotate(text, shift) do
text
|> String.to_charlist()
|> Enum.map( &spin(&1, shift) )
|> to_string()
end
defp spin( char, shift ) when is_lower?( char ), do: ?a + rem( char - 71 + shift, @alphabet_size )
defp spin( char, shift ) when is_upper?( char ), do: ?A + rem( char - 39 + shift, @alphabet_size )
defp spin( char, _ ), do: char
end
Problem
Using Enum is all nice but there is a performance benefit in using streams. As in, according to Elixir In Action, I should only use Enum at the very end to force everything into coming together. Doing it before only makes things slower.
Now you will say “this is a simple app, no need to optimize.” But consider that we are ciphering the entire Bible or any of it’s variants. It’s quite a big book and ciphering it via streams would offer a real benefit.
Question
So my question is:
- Can this example be adapted to use Streams and only use 1 Enum at the end?
Marked As Solved
kokolegorille
Also Liked
NobbZ
Don’t stream just because you heard that it is faster. This is not true!
In many cases streams make it slower!
Please benchmark your use case, before blindly applying a stream.
In a case where you iterate a single time, a stream will probably cost, as you have some overhead during dispatching.
If you have a certain number of stages, you might consider a stream, but should still not blindly use it.
In my opinion, streams do especially play their powers when you would do a lot of “shape shifting” stages, which would flatten a given enum or might drop elements inbetween. A stream here helps a lot to reduce time spent with building lists.
If you want to process a string by char and have concerns about converting the string to a list, map over it andconvert it back again, you should probably use a recursive function that matches on the codepoints and builds a new string in the accumulator, then you might receive some additional optimisation powers from the beam, but beware composed characters!
def f(str, acc \\ <<>>)
def f(<<>>, acc), do: acc
def f(<<c::utf8, str::binary>>, acc), do: f(str, acc <> <<c + 1>>)
dimitarvp
Doesn’t that work?
StringIO.open(text)
|> elem(1)
|> IO.stream(:line)
|> Stream.map(&spin(&1, shift)) # your processing here
# ... more of your processing here
|> Enum.join
The last line directly combines all string pieces into a big string. But you can always use any Enum function istead of that one, depending on your desired end result.
NobbZ
I thought similar to you about a year ago. I learned the hard way during advent of code that streams have a cost.
I learned by benchmarks that they only give a benefit in the above described situations and on very large input that would trigger many GCs when processed using Enum.
A stream always involves additional state keeping, multiple layers of dynamic dispatch, etc. Situation might be totally different when we had a statically typed language that were able to dispatch most of the calls during compile time.
You can learn about my last year’s experience with advent of code when searching this forum.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








