I’ve been enjoying learning ocaml, but I found it very easy to write code riddled with side effects and imperative-ness.

Take this example:

let some_func arg = 
  let input = format_input_from_stdin ()
  let read_content = read_file "some/file/path.txt"
  let to_write = get_write_file_content input read_content
  let () = write_file "some/other/path.txt" to_write 
  let output = run_external_command_with_output 
  (output, read_content)

As you can see, many side effects and imperative steps in the code. Is there a better practice for coding this in a functional manner?

  • xmunk@sh.itjust.works
    link
    fedilink
    arrow-up
    5
    ·
    9 months ago

    I/O isn’t purely functional, ever, us cool functional kids just isolate it from our clean functional code and add exception handling to avoid leaking side effects into the good code.

    • matcha_addictOP
      link
      fedilink
      arrow-up
      1
      ·
      9 months ago

      Ahh I see. So one approach is just to reduce it’s footprint throughout the rest of the code.

      • xmunk@sh.itjust.works
        link
        fedilink
        arrow-up
        1
        ·
        9 months ago

        Yea, in nearly every program we still need some amount of I/O which will violate a purely functional approach - we just try to isolate it as much as possible.

        • matcha_addictOP
          link
          fedilink
          arrow-up
          1
          ·
          9 months ago

          The Haskell approach is to use monads too, right? I can’t seem to understand the benefit of the monad approach

          • oessessnex@programming.dev
            link
            fedilink
            arrow-up
            2
            ·
            edit-2
            9 months ago

            The way you can think of it is that in OCaml everything is implicitly wrapped in an IO monad. In Haskell the IO monad is explicit, so if a function returns something in IO you know it can perform input and output, in OCaml there is no way to tell just from the types. That means that in Haskell the code naturally stratifies into a part that does input and output and a pure core. In OCaml you can do the same thing, however it needs to be a conscious design decision.

          • ornery_chemist@mander.xyz
            link
            fedilink
            arrow-up
            1
            ·
            9 months ago

            True Functional Programmers will probably correct my lack of nuance here, but my understanding is that the IO monad is basically just scribbling some category theory formalities on top of IO ops so that everything is still technically a pure function. You can think of the IO monad as representing the “state” of the rest of the universe outside of your program, which your program reads or modifies. As you pass through your monadic bind ops (i.e., as you read or write IO), the state is carried through implicitly and “modified” as appropriate. All functions, then, are just transforming data (i.e., either your program’s data or the rest of the universe), so everything is pure.

  • solrize@lemmy.world
    link
    fedilink
    arrow-up
    1
    ·
    edit-2
    9 months ago

    For IO, you really can’t. Haskell uses a conceit that your program is a functional state transformer that operates on the contents of the RealWorld (there is an actual object of type RealWorld in the GHC runtime). The idea is that your program’s input is a RealWorld state which contains you looking at a blank screen, and its output is a state where your screen says “hello world”. There are no side effects, just an input and an output. This transformer is then handed off to the runtime which is seen as a simple imperative interpreter separate from Haskell. But of course, under the hood, the above scheme (the “IO Monad”) is just an imperative sub-language within Haskell, whose effectfulness is kept contained by the type system.

    For pure computation things are different, and you use a different set of idioms and data structures than imperative programmers are used to. A familiar example is using recursion instead of iteration. Less obvious is lookup structures: instead of hash tables (which you’d normally update imperatively) you use balanced tree structures where an “update” operation takes an old tree, and returns a new tree that shares most of its structure with the old one, with a few items changed. Then you rely on garbage collection to get rid of old items that are no longer reachable. There is an entire very good book “Purely Functional Data Structures” by Chris Okasaki, about such structures. The book is sort of old now, but still well worth reading.

    For more about how effectfulness is actually handled in Haskell, see the article “Tackling the Awkward Squad” by Simon PJ, https://research.microsoft.com/en-us/um/people/simonpj/papers/marktoberdorf/mark.pdf .