Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

You might benefit from this video [1] on the "Functional Core, Imperative Shell" pattern in Ruby (a very object-oriented language), which sounds like one of the things you are having trouble with.

The talk is about segmenting IO and mutation from the rest of your program. The core of your program should be just pure functions and the IO part of your program should be "at the edge".

For example, consider a game loop. Game loops tend to have a while loop like this:

function loop() { while (should_not_close_window) { update(); // Run update function which updates mutable state draw(); // Draw from the mutable state } }

If we wanted to segment IO, we would return an immutable type/value from the update function instead like:

function loop(old_state) { const new_state = update(old_state); // Call a function which returns a new state given an old_state. draw(new_state); // Draw function still uses IO because drawing is inherently imperative

  if (new_state.should_not_close_window) {
    loop(new_state); // Call the loop function with the new state; this tail recursion replaces the previous while-loop). The program automatically exits the loop if new_state.should_not_close_window is false.
  }
}

The key thing is to turn as much of your program into a function that takes a value and returns a new value as possible, and let the IO code be only there for operations which cannot be done without IO (like drawing).

There are some cases where you might find yourself wanting to perform IO inside the update call but that can be handled by setting some state (inside your root state object) which the boundary of your program interprets as "do IO at the boundary".

For example, let's add an async handler to the loop (async often being IO like disk writes).

// Function to handle async messages function handleAsync(old_state) { for (const msg of old_state.msgs) { execMsg(msg); // Call function to pattern match and execute this specific message } const new_state = remove_msgs(old_state); // Call function that returns same state, except msgs is now the empty list (because the msgs have been handled) return new_state }

Where type msgs could be a list of types like | WriteFile of string | MaximiseScreen

The main loop will be modified by putting a call to this handleAsync function after update, like this:

function loop(old_state) { const mid_state = update(old_state); const new_state = handle_async(mid_state); draw(new_state);

  if (new_state.should_not_close_window) {
    loop(new_state);
  }
}

The majority of your code should be in the pure update() function which returns a new value (this example doesn't explain anything about pure functions at all) and IO should be minimised.

Generally, don't do IO (except at the boundary/root of your application). When you want to do IO, add some kind of message/datatype value to your state object and do the IO when your state object bubbles up, rreturning to the root of your application.

The talk likely explains this better but I hope my attempt at giving an explanation was helpful.

[1] https://www.destroyallsoftware.com/talks/boundaries



Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: