origamitower / folktale

[not actively maintained!] A standard library for functional programming in JavaScript
https://folktale.origamitower.com/
MIT License
2.04k stars 102 forks source link

Error thrown on .ap of Validation #217

Closed jk121960 closed 4 years ago

jk121960 commented 5 years ago

When I use the applicative of Validation the first two predicates work fine but the third always throws an error stating that fn is not a function. Basically on the third in the sequence is passes the target monad rather than the function to the map of the applicative. I did try Gitter but no one had been there since Mar 30th

Steps to reproduce

I just have to add at least 3 predicates to the applicative sequence. the form of this I am using is: I have also noticed that this happens when all of the predicates return "Success" If I have errors is doesn't happen

var isNumberValid = function( target ) { return Success( function( a ) { return function( b ) { return target; } } ) .ap( isEven( target ) ) .ap( isGreaterThan0( target ) ) .ap( isLessThan10( target ) ) // .ap( isOdd( target ) )

};

var t1 = isNumberValid( 8 );

Expected behaviour

in the case of all true from the predicates I expect to receive the original target value back, and that is when it occurs, when I have errors the bug does not show.

Observed behaviour

when all predicates return true the problem manifests whenever I have at least 3 predicates. It occurs on the third when the applicative passes when should be a function in a monad, it passes the original target value in a monad

Environment

(Describe the environment where the problem happens. This usually includes:

Additional information

(Anything else you feel relevant to the issue)

joseaquino commented 5 years ago

Hi @jk121960 the reason why this is failing on the third .ap is because the way Apply works is by evaluating the function wrapped in the Success with the value given to the .ap function which must be a Validation type as well.

So in your sample code, what is happening is that the first .ap will evaluate with the first function wrapped in the Success:

Success( function firstFn( a ) {
    return function SecondFn( b ) {
        // No more functions :(
        return target;
    }
} 

Which returns us a new function that we can evaluate with the second .ap( isGreaterThan0( target ) ) but this second function returns us the value target so by the time we get to the third .ap( isLessThan10( target ) ) we run out of functions that we can "Apply" values to and an error occurs as .ap is expecting to have a function wrapped withing it.

Working code

If you wish to do this, I would advise you to use .concat better, like this:

const isEven = function(num) {
  return num % 2 === 0
    ? Success(num)
    : Failure(["You must provide an even number"]);
};

const isGreaterThan0 = function(num) {
  return num > 0
    ? Success(num)
    : Failure(["You must provide a number greater than zero"]);
};

const isLessThan10 = function(num) {
  return num < 10
    ? Success(num)
    : Failure(["You must provide a numeber less than ten"]);
};

const isNumber = function(num) {
  return typeof num === "number"
      ? Success(num)
      : Failure(["You must provide a number!"]);
}

const isNumberValid = function(target) {
  return Success()
    .concat(isNumber(target))
    .concat(isEven(target))
    .concat(isGreaterThan0(target))
    .concat(isLessThan10(target));
};

isNumberValid(8).matchWith({
  Success: function(result) {
    console.log("Value is valid!", result.value);
  },
  Failure: function(errors) {
    // This will be an array of error messages in the order in which they were added
    console.log("Values failed validation", errors.value);
  }
});

When to use apply

Apply is used whenever you have a curried function which takes multiple arguments, arguments that will come from different Validation instances, like in this example:

const hasValidFirstname = input =>
  input.firstname && input.firstname.trim()
    ? Success(input.firstname)
    : Failure(["The firstname must not be empty"]);

const hasValidLastname = input =>
  input.lastname && input.lastname.trim()
    ? Success(input.lastname)
    : Failure(["The lastname must not be empty"]);

const hasValidAge = input =>
  input.age && typeof input.age === "number"
    ? Success(input.age)
    : Failure(["The age must be a valid number"]);

const areInputsValid = target =>
  Validation.of(firstname => lastname => age => {
    // This will be returned if no Failure happens along the way
    return {
      userInfo: {
        firstname,
        lastname,
        age
      }
    }
  })
    .ap(hasValidFirstname(target))
    .ap(hasValidLastname(target))
    .ap(hasValidAge(target));

areInputsValid({
  firstname: "John",
  lastname: "Doe",
  age: 39
}).matchWith({
  Success: ({ value }) => console.log("Success:", value),
  Failure: ({ value }) => console.log("Failed:", value)
})

Hope this has cleared up things for you :)

jk121960 commented 4 years ago

Sorry for the extremely late reply but I don't remember getting a notification. The ap code I was attempting to use I had gotten originally from the folktale site. I had discovered since then the concat approach on folktale 2. But at least I wasn't doing something stupid and not noticing it. So thanks for the sanity check and I appreciate your explanations thanks very much, I totally get it now.