1

Sorry if my title isn't descriptive enough, I wasn't sure how to phrase the problem... I'm used to programming in C# and have been dabbling in F#. I'm trying to write a function I use in some C# scripts that checks if the string is numeric. I have a F# function written up like this though its not correct according to VS because its expecting an else:

let IsNumeric (entry : string) : bool =     // Function to make sure the data entered is numeric
    for c : char in entry do
        if not ((c >= '0') && (c <= '9')) then
            false
    true

If I put in an else statement and remove the true at the bottom:

let IsNumeric (entry : string) : bool =     // Function to make sure the data entered is numeric
    for c : char in entry do
        if not ((c >= '0') && (c <= '9')) then
            false
        else true

I get this error

FS0001 This expression was expected to have type 'bool' but here has type 'unit'

... if I keep the true at the bottom like in the first code block I get a warning about it returning a bool but should be ignored which I don't quite understand.

FS0020 The result of this expression has type 'bool' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.

This is the C# method I wrote that I've been trying to adapt:

public static bool IsNumeric(string s) //this just makes sure the string is numeric.
{
    foreach (char c in s)
    {
        if (!(c >= '0' && c <= '9') && c != '.' && c != '-')
        {
            return false;
        }

    }
    return true;
}

How should I approach this?

4 Answers4

4

F# does not support "early return" - i.e. you can't stop a function execution in the middle. The code always has to run from beginning to end, completely. And yes, this is a feature, not a bug. It forces you to write cleaner, more understandable and maintainable code.

Generally the way to iterate in functional programming is recursion: you write a function that makes a decision whether to continue iterating or to stop, and in the former case it calls itself, and in the latter case it doesn't.

Recursion is the ultimate foundation of iteration, but for most everyday cases the standard library provides a wide variety of specialized functions, which are themselves built on recursion or on other such functions.

For example, in this particular case, it looks like what you're trying to do is check all characters in the string to see if they're all digits. And look: there is a special function for checking a predicate for every element of a sequence - Seq.forall:

let IsNumeric (entry : string) =
    entry |> Seq.forall (fun c -> (c >= '0') && (c <= '9'))
Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
  • 3
    `Seq.forall` uses less performant iteration than `String.forall`, because `Seq` always uses enumerator-based approach – JL0PD Jul 07 '21 at 03:18
3

F# as most functional languages doesn't support early return. Last evaluated expression is return value.

Canonical solution would be to use recursive function, which compiles to simple loop if you do it right.

let isNumeric (str: string) : bool =
    let rec loop index =
        if index >= str.Length then
            true
        else
            let c = str.[index]
            if (c >= '0' && c <= '9') || c = '.' || c = '-' then
                loop (index + 1)
            else false
    loop 0  

You can check what it compiles to with ILSpy, ILDasm or SharpLab

Most F#-ish is to use

let isNumeric str =
    String.forall (fun c -> (c >= '0' && c <= '9') || c = '.' || c = '-') str

And most correct is to use, because you don't check correctness and ..- would be correct number

let isNumeric (str: string) = System.Double.TryParse str |> fst
JL0PD
  • 3,698
  • 2
  • 15
  • 23
  • For that last example, one might sometimes want to use an overload of TryParse to overrule the current locale settings. More specifically, to programmatically control whether comma or point is to be used as decimal separator. – Bent Tranberg Jul 07 '21 at 05:04
  • @BentTranberg, sure, but if my function would be called `isNumericStringWithCulture` nobody would use it. I think that's better to declare such function as `private` for module with embedded `CultureInfo.InvariantCulture` – JL0PD Jul 07 '21 at 05:27
  • 1
    I was not suggesting you change your answer, but rather adding a piece of information that I know many outside of the English speaking nations frequently need but don't know about. – Bent Tranberg Jul 07 '21 at 05:33
  • Thank you, your answers helped get me the result I was looking for. – emonegarand Jul 07 '21 at 06:44
3

Looks like you're looking for .forall or .exists:

Try it

let isNumericChar (ch: char): bool =
    ch >= '0' && ch <= '9'

let isNumeric (entry: string): bool =
    String.forall isNumericChar entry


printfn "%A" (isNumeric "12345")
printfn "%A" (isNumeric "12a345")

You can also use partial evaluation here:

let isNumericChar (ch: char): bool =
    ch >= '0' && ch <= '9'

let isNumeric = String.forall isNumericChar


printfn "%A" (isNumeric "12345")
printfn "%A" (isNumeric "12a345")
JLRishe
  • 99,490
  • 19
  • 131
  • 169
0

For idiomatic F# code, i would suggest the solution describes by Fyodor Soikin or other users. As F# function has no early return, you either have to use recursion or most often use a built-in function. In your case the Seq.forall is what you want to use.

but still, i want to add another solution that was not mentioned so far. Probalby the most idiomatic conversion of the C# code is, to use a mutable variable. The code would look like this:

let isNumeric (entry : string) : bool =
    let mutable ret = true
    for c : char in entry do
        if not ((c >= '0') && (c <= '9')) then
            ret <- false
    ret

Usually some people will tell you, that it uses a mutable, and isn't functional. But in that case, it isn't really important.

While you should be able to understand recursion, and how to solve it with recursion and immutable variables. This code is still idomatic, and in some cases, easier to understand.

There is no reason to not use mutables in a limited scope.

David Raab
  • 4,433
  • 22
  • 40