2

I am working in TS but will show the tsc -> ES6 code below.

I have a function 'isDigit' that returns true if the the character code is in the range of digits 0-9. This function (isDigit) must be passed as an argument into a higher order function.

const isDigit = (char, charC = char.charCodeAt(0)) => (charC > 47 && charC < 58);

As part of another higher order function, I need to know if a character is NOT a digit. Of course something like below would work...

const notDigit = (char, charC = char.charCodeAt(0)) => !isDigit(char);

BUT it would be more satisfying if I could compose isDigit with another function (I'll call notFun) to apply a not operator to the result of isDigit to make notDigit. In the code below 'boolCond' serves to control if the not operator is applied or not. The code below 'almost' works, but it returns a boolean not a function which does not work when dealing with higher order functions.

const notFun = (myFun, boolCond = Boolean(condition)) => (boolCond) ? !myFun : myFun;

As is usually the case, while preparing this question I wound up finding an answer, so I will share my answer and see what improvements come from the community.

The issue observed above (getting a boolean instead of a function) is an issue of 'functional composition, I found several optional approaches in the post of Eric Elliot, from which I selected the 'pipe' functional composition method.

see Eric Elliot's excellent post

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

The implementation of this pipe composition function looked like the below TS... For those following along at home, I have included the recursive count while 'recCountWhile' function that is the ultimate consumer of the composed (i.e. piped) function (please excuse the inverted order that these functions appear but this was done for clarity).

export const removeLeadingChars: (str: string) => string = 
  (str, arr = str.split(''), 
   dummy = pipe(isDigit, notFun), cnt = recCountWhile(arr, dummy, 0)) => 
          arr.slice(cnt).reduce((acc, e) => acc.concat(e),'');

export const recCountWhile: (arr: string[], fun: (char: string) => boolean, sum: number) => number = 
  (arr, fun, sum, test = (!(arr[0])) || !fun(arr[0]) ) => 
      (test) ? sum : recCountWhile(arr.slice(1), fun, sum + 1);

The result is a composed function 'removeLeadingChars' that composes the 'isDigit' with the 'notFun' (using the pipe function ) into 'dummy' function that is passed to the recCountWhile function. This returns the count of 'not digits' (i.e. characters other than digits) that lead the string, these characters that are then 'sliced' from the head of the array, and the array is reduced back to a string.

I would be very keen to hear about any tweaks that may improve on this approach.

Elias Soares
  • 9,884
  • 4
  • 29
  • 59

1 Answers1

2

Good on you to find your answer and still post the question. I think this is a nice way to learn.

For the sake of a function composition exercise, here's how I might structure your functions.

see Keep it simple below for how I would handle this with practical code

const comp = f => g => x => f(g(x))

const ord = char => char.charCodeAt(0)

const isBetween = (min,max) => x => (x >= min && x <= max)

const isDigit = comp (isBetween(48,57)) (ord)

const not = x => !x

const notDigit = comp (not) (isDigit)

console.log(isDigit('0')) // true
console.log(isDigit('1')) // true
console.log(isDigit('2')) // true
console.log(isDigit('a')) // false

console.log(notDigit('0')) // false
console.log(notDigit('a')) // true

Code review

Btw, this thing you're doing with the default parameters and leaked private API is pretty wonky

// charC is leaked API
const isDigit = (char, charC = char.charCodeAt(0)) => (charC > 47 && charC < 58);

isDigit('9')     // true
isDigit('9', 1)  // false   wtf
isDigit('a', 50) // true    wtf

I understand you're probably doing it so you don't have to write this

// I'm guessing you want to avoid this
const isDigit = char => {
  let charC = char.charCodeAt(0)
  return charC > 47 && charC < 58
}

... but that function is actually a lot better because it doesn't leak private API (the charC var) to the external caller

You'll notice the way I solved this in mine was to use my own isBetween combinator and currying which results in a pretty clean implementation, imo

const comp = f => g => x => f(g(x))

const ord = char => char.charCodeAt(0)

const isBetween = (min,max) => x => (x >= min && x <= max)

const isDigit = comp (isBetween(48,57)) (ord)

More of your code that does this awful default parameters thing

// is suspect you think this is somehow better because it's a one-liner
// abusing default parameters like this is bad, bad, bad
const removeLeadingChars: (str: string) => string = 
  (str, arr = str.split(''), 
   dummy = pipe(isDigit, notFun), cnt = recCountWhile(arr, dummy, 0)) => 
          arr.slice(cnt).reduce((acc, e) => acc.concat(e),'');

Try to avoid compromising the quality of your code for the sake of making everything a one-liner. The above function is much worse than this one here

// huge improvement in readability and reliability
// no leaked variables!
const removeLeadingChars: (str: string) => string = (str) => {
  let arr = str.split('')
  let dummy = pipe(isDigit, notFun)
  let count = recCountWhile(arr, dummy, 0)
  return arr.slice(count).reduce((acc, e) => acc.concat(e), '')
}

Keep it simple

Instead of splitting the string into an array, then iterating over the array to count the leading non digits, then slicing the head of the array based on the count, then finally reassembling the array into an output string, you can... keep it simple

const isDigit = x => ! Number.isNaN (Number (x))

const removeLeadingNonDigits = str => {
  if (str.length === 0)
    return ''
  else if (isDigit(str[0]))
    return str
  else
    return removeLeadingNonDigits(str.substr(1))
}

console.log(removeLeadingNonDigits('hello123abc')) // '123abc'
console.log(removeLeadingNonDigits('123abc'))      // '123abc'
console.log(removeLeadingNonDigits('abc'))         // ''

So yeah, I'm not sure if the code in your question was merely there for an exercise, but there's really a much simpler way to remove leading non digits from a string, if that's really the end goal.

The removeLeadningNonDigits function provided here is pure function, does not leak private variables, handles all inputs for its given domain (String), and maintains an easy-to-read style. I would suggest this (or something like this) compared to the proposed solution in your question.


Function Composition and "Pipe"

Composing two functions is usually done in right-to-left order. Some people find that hard to read/reason about, so they came up with a left-to-right function composer and most people seem to agree that pipe is a good name.

There's nothing wrong with your pipe implementation, but I think it's nice to see how if you strive to keep things as simple as possible, the resulting code cleans up a bit.

const identity = x => x

const comp = (f,g) => x => f(g(x))

const compose = (...fs) => fs.reduce(comp, identity)

Or if you'd like to work with my curried comp presented earlier in the post

const identity = x => x

const comp = f => g => x => f(g(x))

const uncurry = f => (x,y) => f(x)(y)

const compose = (...fs) => fs.reduce(uncurry(comp), identity)

Each of these functions have their own independent utility. So if you define compose in this way, you get the other 3 functions for free.

Contrast this to the pipe implementation provided in your question:

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

Again, it's fine, but it mixes all of these things together in one function.

  • (v,f) => f(v) is useful function on its own, why not define it separately and then use it by name?
  • the => x is merging the identity function which has countless uses
Community
  • 1
  • 1
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Honestly, this is only scratching the surface with all the doors you've opened with your question. If you have any other questions or want further explanation on the topics presented here, just let me know ^_^ – Mulan Feb 07 '17 at 21:01
  • 1
    this is great, really simple, the question was hard to understand but the answer made me understand what was actually the problem – Oscar Franco Feb 07 '17 at 21:35
  • @naomik ; Many thanks for your detailed review and commentary. I likely should have prefaced my 'question' with the caveat 'I am entirely self taught, and very stubborn in my ill-informed opinions' :) with this in mind I am hoping you can point me to some material that will help me better understand why "abusing" the default parameters is a bad thing and what might be bad about a leaked private API (specific to this case). I have reviewed the Mozilla entry on Default_parameters and I would be keen to know if you shed light on if they are illustrating how to 'abuse' this feature. Thanks+++ –  Feb 08 '17 at 14:52
  • @naomik; I think I better understand what your point when you say I my 'isDigit' function "leaked private API is pretty wonky". I now understand that your were pointing out that the argument "charC" is 'leaked' (i.e. can be provided via the function call as you did in the case of "isDigit('9', 1) // false wtf" –  Feb 08 '17 at 15:34
  • 1
    @naomik; I think I better understand what your point when you say I my 'isDigit' function "leaked private API is pretty wonky". I now understand that your were pointing out that the argument "charC" is 'leaked' (i.e. can be provided via the function call as you did in the case of "isDigit('9', 1) // false wtf" where charC is assigned to the value 1 and the fallback assignment is skipped. It may help to point out that the source code is in Typescript and the tsc compiler will only allow one argument for the function 'isDigit' else the signatures don't match. Again many thanks. –  Feb 08 '17 at 15:45
  • @angerular-steve i use the word "abuse" because you're only using the default parameter as a means to avoid more than one line in the function body. Parameters are meant to affect a function's output, but in this case, you have some parameters that render the function broken (as demonstrated by `isDigit('9', 1)`), or at the very least, would severely confuse the caller of the function. – Mulan Feb 08 '17 at 16:01
  • @angerular-steve By contrast, here's a properly used default parameter: `const split = (str, x = '') => str.split(x)`. Here, we could call with either `split('abc')` or `split('abc', 'b')` and the latter call can't somehow *break* the function or cause confusion to the user. You could think of default parameters as *sensible defaults* but always remember they're still meant to be parameters that can be controlled by the caller. – Mulan Feb 08 '17 at 16:02
  • @angerular-steve in short: so using default parameters as a means of replacing private/local variable assignments within your function body is a form of abuse, and actually opens your program to strange/confusing behaviours. Sure, TS signatures will help prevent against misuse, but that doesn't change the fact that a language feature was used in a very idiosyncratic way. Remember, TS ultimately compiles to JS too, so the resulting JS would be fragile. – Mulan Feb 08 '17 at 16:07
  • @naomik Many thanks, I have learned quite a lot from our discussion and will take much of what you advised to heart (also really appreciate the elegant composition functions). To return to my original reason for asking the question, I thank you again for an improved (or at least more flexible) composition function. I trust that I did not come off as defensive or difficult, in my responses; apologies if I did. Best regards, Steve –  Feb 08 '17 at 17:34
  • @angerular-steve I'm delighted to have helped in any capacity. It was pleasant interacting with you. Feel free to reach out any time in the future ^_^ – Mulan Feb 08 '17 at 17:49
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/135218/discussion-between-angerular-steve-and-naomik). –  Feb 08 '17 at 21:27