0
votes

I'm trying to implement a recursive function to remove empty directories in purescript. For the following code I get an error about matching Effect with Array.

module Test where

import Prelude

import Data.Array as Array
import Effect (Effect)
import Node.Buffer.Class (toArray)
import Node.FS.Stats (isDirectory)
import Node.FS.Sync as FS
import Node.Path (FilePath)
import Prim.Boolean (False)

rmEmptyDirs :: FilePath -> Effect Unit
rmEmptyDirs path = do
  stats <- FS.stat path
  if isDirectory stats then do
    files <- FS.readdir path
    if Array.length files == 0 then
      FS.rmdir path
    else do
      file <- files
      rmEmptyDirs file
  else
    pure unit

Here is the error message:

Could not match type
Effect
with type
Array
while trying to match type Effect Unit
with type Array t0
while checking that expression rmEmptyDirs file
has type Array t0
in binding group rmEmptyDirs
where t0 is an unknown type

I understand that the innermost do block is in an Array context. I don't know how to "strip off" the Effect from the recursive call to rmEmptyDirs. Putting Array.singleton $ before the call doesn't help. liftEffect has the opposite effect of what I want to do. How do I get this compile?

1

1 Answers

4
votes

The standard way to thread one context through another is traverse.

Look at the type signature:

traverse :: forall a b m. Applicative m => (a -> m b) -> t a -> m (t b)

First you give it a function a -> m b - in your case that would be rmEmptyDirs with a ~ FilePath, m ~ Effect, and b ~ Unit. Then you give it some container t (in your case Array) full of a (in your case FilePath). And it runs the function on every value in the container, and returns the same container full of resulting values b, the whole container wrapped in context m.

In practice this would look like this:

traverse rmEmptyDirs files

Then you'd also need to throw away the array of units, or else the compiler would complain that you're implicitly discarding it. To do that, either bind it to a throwaway variable:

_ <- traverse rmEmptyDirs files

Or use the void combinator, which does the same thing:

void $ traverse rmEmptyDirs files

Another useful thing is for, which is just traverse with arguments flipped, but the flipped arguments allow you to seamlessly pass a lambda-expression as the argument, making the whole thing look almost like a for statement from C-like languages. Very handy for cases when you don't want to give a name to the function you're using to traverse:

for files \file -> do
  log $ "Recursing into " <> file
  rmEmptyDirs file

Finally, unrelated hint: instead of if foo then bar else pure unit, use the when combinator. It would allow you to drop the else branch:

when (isDirectory stat) do
  file <- FS.readDir ...
  ...

And instead of length ... == 0 use null:

if Array.null files then ...

For Array this doesn't matter, but for many other containers length is O(n) while null is O(1), so it's good to build the habit.