Picking up FP coming from OOP (split thread)

Hm. Programming in elixir was a breeze for me. @nickjanetakis that code really does feel like you’re trying to shove for loops into an Enum method. They’re similar, but not quite the same, and there are some very reasonable ways you could refactor that code to make it less brain-breaky.

But you’re right, it’s not always easy. I am desperately trying to hire someone for my team and unfortunately other people in the company have a say in who we hire. I keep telling them that I’d rather have someone inexperienced than someone experienced because sometimes (not always) it’s easier to teach something to someone from scratch than have them unlearn something, especially when it’s 10 degrees off-axis (versus 90 degrees off-axis). But sometimes getting management and other developers to listen to you is like f-ing pulling teeth.

In some ways I think that’s why people take to the erlang alien syntax because at the point where you go into it, you’re already invested in giving up everything you’ve learned and starting from scratch in a visceral sense. On the other hand, erlang has always had a growth problem so… “No right answers” abound.

3 Likes

To be honest I don’t think that the ability to pick up Elixir quickly (and by extension FP) is related to being smarter.

From my personal experience, which is of course only anecdotal, I’ve come to the conclusion that picking up the FP paradigm becomes harder the more experience somebody has with a different paradigm. I’ve seen juniors pick up Elixir super fast and I’ve seen seniors really struggle with it. It seems as if all of that precious implicit knowledge which makes one so productive with one’s chosen tools, actually is a hindrance when you try to break out of that familiar paradigm and learn a new one.

And from what I can gather from it seems that you have a lot of experience in a paradigm other than FP. While it’s easier said than done: try to take a step back and don’t ask yourself “how would I solve this in Python” but rather learn anew, learn naively.

But this is kinda derailing the thread, so maybe we could move it somewhere else?

4 Likes

I understand the frustation, but that’s a bit uncalled for. I never implied that i was great at anything, just that for me, Elixir was something my brain made sense of. I definitely can understand that it’s not the same for everybody. What you find clear with Python, it’s unlikely to be the case for me, that’s all. Now, don’t take this at me saying that you’re not made for Elixir. But has @ityonemo said, it does feel like you are trying to force things that Elixir wasn’t made for which then means that no effort towards making that kind of code better was made by the language makers.

Concerning your problem, i just had a few minutes, but i’d start like this, while refactoring, to make it more clear and concise:

def set_positions(course) do
    {:ok, agent} = Agent.start_link(fn -> %{section_idx: 1, lesson_idx: 1} end)

    change_acc = fn key ->
      Agent.get_and_update(agent, fn m -> {m[type], Map.update!(m, key, &(&1 + 1))} end)
    end

    sections =
      course.sections
      |> Enum.map(fn %{lessons: lessons} = section ->
        %{section | 
             position: change_acc.(:section_idx), 
             lessons:  Enum.map(lessons, &%{&1 | position: change_acc.(:lesson_idx)}), 
             lesson_count: Enum.count(lessons)}
      end)

    %{course | sections: sections, lessons: [], videos: []}
end

Note that i didn’t implement the reset parameters (despite it’s name, not entirely sure from the way you are using it of the wanted result ?), and i did this piece of code on the forum directly. So it’s clearly not clean or tested (and i have like 45 days of experience with Elixir so might be doing this wrong too :grin:). In addition, in any app, i would have app wide functions to handle handling state for that type of common needs (incrementing integers, so the part about Agent just wouldn’t be there).

(note: let’s not hijack the post subject any further though)

1 Like

You must be a lot smarter than me.

I’ve come from VB6 -> PHP -> Ruby / Python and learning Elixir has been one of the most challenging things in my entire development career.

I haven’t thrown in the towel but I still find myself getting 100% stuck and having to ask for help on how to solve things that I can solve in minutes with Python without thinking. Simple things like nested loops with state, and other common things you wind up having to do in projects.

That’s my main gripe with the language. It’s feeling so dependent on others. I’ve asked more questions about how to do things in Elixir in a few months of casually writing a side project with a few thousand lines of code than I have in a 20 year stretch of development and having written probably a million lines of code at this point across 150+ projects in various other techs.

I look at code like this and it absolutely crushes my brain. But in Python this would have been a few lines of code without even thinking twice.

  def set_positions(course, reset_lesson_count \\ false) do
    sections =
      Enum.map_reduce(Enum.with_index(course.sections, 1), 0, fn {section,
                                                                  section_i},
                                                                 acc ->
        {lessons, acc} =
          Enum.map_reduce(Enum.with_index(section.lessons, 1), acc, fn
            {lesson, lesson_i}, acc ->
              position =
                if reset_lesson_count do
                  lesson_i
                else
                  acc + 1
                end

              {%{lesson | position: position}, acc + 1}
          end)

        {%{
           section
           | position: section_i,
             lessons: lessons,
             lesson_count: Enum.count(lessons),
             duration_human:
               LMS.Seconds.to_human(
                 LMS.Videos.sum_section_duration_seconds(section.id)
               )
         }, acc}
      end)
      |> elem(0)

    %{course | sections: sections, lessons: [], videos: []}
  end

This is something that took over a week to write and about 100 lines of chat on IRC with Jose himself. I think no matter what, I’ll never fully understand this style of coding. But the Python equiv. is like 10 lines of code with 2x for in loops.

Not saying Python is better, but I can see for sure how some folks try to pick up Elixir and bail out for another language instead.

Edit: Here’s the Python solution for comparison. The ~10 lines is the set_positions function. I included the data structure and printing the output to make it easier to reproduce locally if anyone wanted to run it. You can run it with python3 set_positions.py.

# set_positions.py

course = {
  'sections': [{
      'position': 0,
      'lessons': [{
          'position': 0
        },
        {
          'position': 0
        }
      ]
    },
    {
      'position': 0,
      'lessons': [{
          'position': 0
        },
        {
          'position': 0
        }
      ]
    }
  ]
}

def set_positions(course, reset_lesson_count=True):
    section_counter = 1
    lesson_counter = 1

    for section in course['sections']:
        section['position'] = section_counter
        section_counter += 1

        for lesson in section['lessons']:
            lesson['position'] = lesson_counter
            lesson_counter += 1

        if reset_lesson_count:
            lesson_counter = 1

    return course

print('Listing positions with resetting the lesson positions:')
print(set_positions(course))

print()

print('Listing positions without resetting lesson positions:')
print(set_positions(course, False))
1 Like

@nickjanetakis As others have alluded, perhaps this experience is counter productive. A mutable counter helps but your problem is actually a natural fit for pattern-matching and recursion.

Not 5 lines of Python, but here’s my take in 5 functions of Elixir.

Edit: Code updated to match Python sample above.

def set_positions(course, reset \\ true) do
  sections = update_sections(course.sections, 1, 0, reset)
  %{course | sections: sections}
end

def update_sections([], _, _, _), do: []

def update_sections([section | tail], section_acc, acc, true) do
  {lessons, _acc} = Enum.map_reduce(section.lessons, acc, &update_lesson/2)
  section = %{section | position: section_acc, lessons: lessons}
  [section | update_sections(tail, section_acc + 1, 0, true)]
end

def update_sections([section | tail], section_acc, acc, false) do
  {lessons, acc} = Enum.map_reduce(section.lessons, acc, &update_lesson/2)
  section = %{section | position: section_acc, lessons: lessons}
  [section | update_sections(tail, section_acc + 1, acc, false)]
end

def update_lesson(lesson, position) do
  lesson = %{lesson | position: position + 1}
  {lesson, lesson.position}
end
5 Likes

Thanks. Turns out it was 10 lines not 5 (the code was written about 6 months ago), but I did edit in the Python code just so there’s a direct comparison.

It’s hard to say if the experience is counter productive. I asked a few real life friends who aren’t developers and walked them through the Python version. They were able to understand it well enough to get it. It was also super easy to explain.

I didn’t do the same with the Elixir version because I don’t know it well enough to explain it, but I did show them the code. They mostly joked around like “nah, let’s do something else”.

For me, the ideal solution is readable, explainable, concise and fast. Sometimes the least amount of code isn’t the best solution but generally speaking less code is good. If you replace 10 lines with 20 lines in 100 different examples, you end up with a ton more code written.

Lines of code is a secondary metric to readability. Number #1 metric is “make the code read like a rough English description of the actual problem you are solving”. Many languages, Elixir and LISP included, allow you to go extreme and minimize coding lines to ridiculously low numbers but then your code ceases to be understandable from the first glance, including for your future self.

In that regard @jayjun’s solution strikes me as the best because it (1) breaks down the problem to separate functions with clear names and (2) utilizes pattern-matching to reduce coding lines which would otherwise explode due to usage of if/else in non-FP languages and (3) uses idiomatic Elixir so people versed in it will quickly understand it.


I do agree FP makes mutable data a bit harder than many people feel that they should be. But think of it this way: you should absolutely avoid mutable state for as long as you can and then make it an explicit opt-in so it’s noticeable. It’s about the discipline to maximally utilize pure functions and only use impure ones when you have no choice (dealing with the real world and its changing state). This is a blessing for long-term maintainability which of course comes at the cost of living and breathing FP.

In your case I believe it’s a case of familiarity and being tempted to go the easy road: do the job now and think about potential problems later. But don’t underestimate taking N shortcuts in bigger projects – the complexity and the hidden implications of the “easier” code build up and you quickly end up with a project that’s hard to evolve.

2 Likes

I wouldn’t say my solution is the easy way out. I didn’t even write it, Jose it. I had to write a literal blog post of comments just to remember what it did at the time of him explaining it to me.

It was the result of hitting a brick wall when trying to solve the problem of iterating over a nested loop with a counter that can be situationally reset based on a boolean.

IMO, the Python version is a lot more maintainable to me. It’s only a handful of lines of code and it’s really straight forward on what each line does – even if you don’t know Python, you can see how it works at a glance.

I spent an hour breaking down jayjun’s solution and I still couldn’t figure out what was going on without dropping a puts on nearly every single line. It’s not that it’s bad, it’s just a much more complex solution than what you could do in Python (not a fault of jay’s, but it’s just how Elixir is I suppose).

And as a developer I think this might be partly why a lot of folks try Elixir out and then leave (this reply was originally in a different thread about companies using Elixir and leaving and got moved here). When you run into this type of scenario a whole bunch of times, it leaves you with 0 confidence in being able to solve problems. Not necessarily this exact problem, but overall problems where you’re like “I have no idea how to solve this after Googling for 4 hours and asking for help everywhere”, when you know you could solve it in a different language in 5 minutes.

It is your personal experience, don’t make it a general rule…

I also started FP with Elixir, coming from OOP, and translating some of my previous code was really helpful.

Trying to solve a problem in FP with OOP mind will not help You. Do not try to bend the spoon, instead try to bend your mind :slight_smile:

I can do things in Elixir that are simply impossible for me to write in Python (concurrency related). I can do things in Python that would require much more work in Elixir (AI for example). Pick up the tool for what You need…

I can read and write in french, but I cannot read in chinese. What conclusion can I draw from this? None.

4 Likes

That’s a learning process and it’s completely okay to do until the whole thing clicks in your mind. It means you are still learning Elixir, and likely FP in general.

No. It means “it’s more complex for my Python-oriented mind”, nothing else.

Don’t cherry pick. Many people said companies leave Elixir due to small hiring pool and thus insecurity about who is going to maintain projects after some of their programmers leave.

Familiarity, as I said. And as @kokolegorille said, pick the best tool for any given problem. Nobody here claims Elixir is the end-all-be-all solution. There are some problems where imperative languages work better – like mutating a lot of state in short amounts of time; my Rust code for such problems beats Elixir both in terms of raw speed and readability / understandability.

But please, don’t generalize.

2 Likes

@nickjanetakis, I feel your frustration from time to time, coming from ~20 years of Java development. I’ve dabbled on and off with FP in my spare time for years and I have a long way to go.

But seeing solutions like the one from @jayjun gives me hope that I can tackle most challenges in a concise, readable FP way once I get more practice and experience (although the Java part of my brain immediately wanted to add type specs to @jayjun’s code. :stuck_out_tongue: ).

For me, learning about FP after writing so much imperative code in my career has changed the term paradigm shift from a cliche to a very real experience.

2 Likes

This. Has i said before, i have less than 2 months of experience with Elixir. My app contains a truckload of complex calculations, and FP (and Elixir) have given me the ability to keep my sanity while handling a complex list of case / scenarios, something that imperative code never got close too.

I didn’t come up with @jayjun solution, mostly because i use shared state (between Agent and ETS tables, two things really awesome about Elixir / Erlang once you get the hang of it) has i need it in my app, and was trying to come up with a single function solution simpler than what you had (could have used list comprehensions to go further down that road). But honestly, i have no issues whatsoever reading his code. I really don’t mean anything by this, other than it clicks with me. And has @Ted said, down the road, when you keep adding complexity, Elixir keeps your sanity at bay.

I do think @nickjanetakis, that you have you’re finger on something pretty clear about Elixir, has it’s pretty hard to argue the simplicity of simple code made in Python (one of it’s strong selling point) against Elixir in your example, when you don’t know Elixir or the FP paradigm in general. But that’s the other post subject this one originated from. But on the learning subject, i’d say, keep hitting that wall and come back to ask questions on this forum, has the awesome people populating have clearly demonstrated they’ll go out of there way to help you out :sunglasses:, and i firmly believe that eventually, it will make sense to you too.

To offer an analogy, and rephrase some of what’s been said here, the brick wall you are running into is not one of your own intellect, it’s one of your own experience. The wall you are hitting is a load-bearing foundation of your experience with OOP.

Just learning how to understand a program the first time is really hard. And to get started you really have to accept whatever paradigm you are learning under as Just How Things Work. Eventually, you’ll begin to understand the paradigm itself, and then common patterns around it, then frameworks using it, etc; building on that foundational understanding higher and higher.

Trying to learn a totally new paradigm is even harder: you have to climb down from all the abstractions and patterns you’ve built a deep understanding of, back to the ground floor. Then, you’ll keep running into these walls everywhere, those foundational Just How Things Work assumptions you forgot you were relying on when you were dancing amidst abstractions. They just get in your way now, and slow you down more than when you were last on the ground floor!


I spent an hour breaking down jayjun’s solution and I still couldn’t figure out what was going on without dropping a puts on nearly every single line.

I had to do this when I was first learning Elixir. I also did this extensively when I was first learning programming, the two experiences are very similar during a major paradigm shift! But in OOP you got used to moving so much faster without such rudimentary crutches, so it feels far less bearable now.


TL;DR: Your frustrations are common growing pains when learning a very different language, which can be much harder than learning any language for the first time. Hang in there! It gets easier, we’re here to help, and your investment in building a new worldview isn’t going anywhere if you decide to take a break for a while and return to the cathedral of OOP understanding you’ve already spent years building for yourself. :slight_smile:

5 Likes

Right, but in this case it would seem weird to create a Python microservice for 1 function. That would create even more complexity in the end. When building a real world application you’re often tasked with having to solve many different types of problems, and the best tool for the job is usually the language that has reasonable ways to solve all or most of those problems with the least amount of resistance.

I’m not but I do think a reason why there’s hiring issues is because not enough people use it. If more people used the language then there would be more people to hire. The question now is why aren’t more people using it?

One hunch I have is developers try it and then they hit road block after road block, and instead of relentlessly asking questions, they disappear and go back to whatever they were using before and for most use cases (web dev), that’s good enough for them because it’s good enough for the thousands of companies using those other languages.

Yeah I’ve been posting here for years. It’s just hard to stick with Elixir for me at least. The community support is great, but it’s really tiring when you run into situations where you can’t figure things out on your own or through Google in read-only mode (reading docs, SO, forum posts, etc.).

You end up spending days trying to solve what feels like trivial things you’ve solved many times in other languages, and that saps you of all motivation to write code. And when this happens again and again and again it’s impossible for it to become an enjoyable experience where you’re in a flow state crushing your goals. Instead it becomes like an experiment where you get electrocuted every time you press a button and you avoid it.

That’s what’s happened to me a few times now with Elixir. Write a couple thousand lines of code for a few weeks, get stuck a million times along the way. Get burnt out, don’t touch it for 6 months, talk myself into thinking things will be better next time, try it again, realize you have to re-learn almost everything you’ve learned last time and here we are ~2 years later.

Yeah the issue here is I’ve written a decent amount of Elixir. Not a lot but a few thousand lines while working on a side project. There’s just something about everything that doesn’t click with how I think no matter how much effort I put in.

I can walk through something, write some code and in a week have almost no idea what was going on unless I carefully spend a lot of time re-tracing everything. I don’t remember having these issues with any other language I learned. It’s mainly around code like I originally posted that leans heavily on enum, map, reduce, etc…

It feels like there’s a hurdle I haven’t cleared yet and there’s no sign on how high this bar is, but feels like it’s out of reach at the moment.

1 Like

Have you tried going through the entire Exercism Elixir track? That’s a hardcore training. :slight_smile:

First, kudos to you for sticking with it for so long. Which brings me to this question. What makes you come back ? There has to be things in Elixir that bring you back again and again. I’m just curious, and that’d be interesting to know.

Every so often, someone from an OOP background comes by the forum feeling frustrated. The response tends to be something along the lines of “FP/Elixir isn’t hard, you’re just looking at it wrong.” This is perhaps true, but also unhelpful. Isn’t the challenge of looking at it right precisely what makes it hard? If you think it is hard, then it is. Your experience is, by definition, correct. Here are some thoughts:

  1. It’s bizarre to discuss these things as if they are 100% nurture and 0% nature. Brains work differently. We all have different talents and predilections. Is painting harder than skateboarding? Yes, perhaps your experience has ingrained OOP in a way that is hard to unlearn, but it’s also possible that OOP naturally suits the way you think.

  2. Since change is universally difficult, you need a reason to learn new things, particularly difficult things. The most important question is: What does Elixir/FP offer you that Python doesn’t? This particular piece of code is easier for you in Python, but is that true in all situations? Does Elixir/BEAM/FP simplify or facilitate anything for you? Of course we could enumerate all the textbook benefits of the BEAM and its ecosystem, or FP generally, but it only matters if those things hold value for you personally. Specifically, it matters if they hold value relative to tools you already have.

  3. Simple versus complicated is very subjective. LoC is definitely a poor metric for this. People who are used to FP might find a lot of logic in one function to be inherently complicated. The first time I saw a bodyless clause with a bunch of function overloading in Elixir, it looked very complicated to me. But now I feel that writing a lot of functions reduces the complexity of the functions themselves. Now, when I see long imperative functions, it feels more ‘complicated’ than it used to.

  4. Sometimes you need to complicate simple things in order to simplify complicated things. I think immutability falls into this space. I recently posted a question on this forum where I hit an immutability snag, so I can identify with finding it challenging. But immutability mitigates a lot of danger as complexity climbs, eventually becoming a source of comfort.

  5. It’s worth exploring other functional languages, to determine if FP itself fits poorly with your thought process, or if FP comes easily, but Elixir does not. If people are struggling only with Elixir, the community needs that feedback. We can work on improving tutotials and docs to address the pain points.

  6. It has been mentioned already, but exercism.io is a great way to force yourself to visit areas of a language you wouldn’t otherwise see. It has helped cure me of the belief that all things should be possible via the Enum module.

  7. This is a recurring topic on the forum. I wonder if the forum/community would benefit from a dedicated section. Something along the lines of An introduction to Elixir for object-oriented programmers, with a tutorial and accompanying thread for discussion.

11 Likes

Nope, I’ve never been an academic learner. Every language or framework I’ve learned came from working on a real world web app project. I often start from the top and work my way down by trying to solve problems as they come up like “how do you paginate results?” and things like that.

It’s worked in every other language / framework so far. For reference it took about 3 months to learn Ruby / Rails from scratch to the point where I was comfortable building large apps for clients that involved implementing things I didn’t know how to do initially. This was like 5 years ago.

With Flask, I had already knew Python and it took about the same amount of time to get a handle on things (less opinions = more ramp up time since you can get into yak shaving scenarios more frequently).

It’s hard to describe. I like everything about the language except for the language itself is maybe the most concise way to say it. I think a lot of things were very well thought out that overall make it a nice environment to work in. Everything feels pretty cohesive as an ecosystem, but the IMO (and this is very much an opinion) is the syntax can be confusing as hell sometimes, super verbose and often times the performant solution is the least readable and is the most non-obvious / complex solution.

For example comparing the amount of code you need to write in Ecto vs ActiveRecord. It also doesn’t help that there’s often 10 different ways to do something in Ecto and none of them are really explained in the docs clearly on why you would use X over Y.

But generally speaking solving problems in Python vs Elixir often requires a lot more code in Elixir to get a solution that is a good balance between maintainability and performance.

There was another example I posted like a year ago around generating 5,000 random codes with Elixir. The easy to reason about code ended up being insanely slow and the performant solution ended up being something I would have never written in a million years. But the Python solution was both concise and fast and super straight forward to reason about. There’s just tons of examples of that.

And it all adds up because when you’re trying to ship something, hitting a brick wall for 3 days to try and understand, ask questions, have discussions and optimize something that could have taken 15 minutes with Python is a real bummer.

Even when learning Ruby and Python, it was never like that (at least not for me). It was like “oh yeah, of course you would write like this” and that solution ended up being easy to reason about and fast. I very rarely had to 2nd guess every implementation and wonder “I wonder if this is the right way to do this”.

These are good questions and I’ve asked myself them many times.

I think for the types of web apps I build (typical web apps with tens of thousands of daily visitors that do things with a database and process things outside of the request / response cycle) Python is on equal footing with Elixir from a language perspective.

I mean, I’ve had Python apps running on a web server for 8 months straight without being restart with no memory leaks, processing millions of jobs through Celery (a background worker tool in Python that could be somewhat comparable to Oban except it uses Redis instead of PostgreSQL) and so on.

It also used very little memory and web requests were served in 50ms or less without any type of caching. The ecosystem is good (lots of official libraries from companies like Stripe, tons of user libs, infinite learning material, etc.).

I understand the benefits of the BEAM but in practice I think you can get by without them and it’s “good enough”. Just like Rails is good enough to build Shopify, GitHub and an email service (https://hey.com).

For scaling, there’s always container orchestration tools, so I would consider horizontal scaling a stateless app a solved problem with whatever stack you choose.

It’s hard to offer something so compelling that the masses switch because it’s so much easier to fall back to other solutions that are good enough, and perhaps even better if it means being able to ship something with the tech stack you know in 3 months instead of 3 years.

Another important thing to ask yourself is if you’re building something to learn a new technology or are building something to actually ship it and see if it’s a viable product / service.

At the micro level (a function) maybe, but one of the biggest appeals of Rails was “holy crap, look at the code you didn’t have to write”. There is a case for being able to replace a 60,000 line app with a 25,000 line app, especially if those 25k lines are super readable and “fast enough”.

Keep in mind, this isn’t necessarily abstraction choices or batteries includes vs not. It’s language level syntax.

With Elixir I often find myself writing a lot more code than with Python, and more code means more surface area and more things to actively think about. It’s like death by a thousand paper cuts when you find yourself having to write 20 lines of code to do the same thing you could have done in 10 (meanwhile the 10 line solution is still very readable and fast).

1 Like

You’ve been given various arguments which you continually dismiss with generalizing that Elixir is hard no matter what you tried with it – and then you refuse to learn because it’s “academic”. Both are non-charitable and unfair views.

If you feel Python is treating you better, every time, then there’s no point in retrying to learn Elixir as well. I have a problem with you rejecting good and balanced arguments (plus you are repeatedly generalizing even though you have been offered the point of view that Elixir is consistently hard for you and not everyone) – but we seem to be talking past each other so I’m bailing out in order not to pollute the discussion.

3 Likes