Reputation: 103
I was attempting to convert this to F# but I can't figure out what I'm doing wrong as the error message (in title) is too broad of an error to search for, so I found no resolutions.
Here is the code:
let getIP : string =
let host = Dns.GetHostEntry(Dns.GetHostName())
for ip in host.AddressList do
if ip.AddressFamily = AddressFamily.InterNetwork then
ip.ToString() // big fat red underline here
"?"
Upvotes: 2
Views: 3177
Reputation: 36718
A for
loop in F# is for running imperative-style code, where the code inside the for
loop does not produce a result but instead runs some kind of side-effect. Therefore, the expression block in an F# for
loop is expected to produce the type unit
, which is what side-effect functions should return. (E.g., printfn "Something"
returns the unit
type). Also, there's no way to exit a for
loop early in F#; this is by design, and is another reason why a for
loop isn't the best approach to do what you're trying to do.
What you're trying to do is go through a list one item at a time, find the first item that matches some condition, and return that item (and, if the item is not found, return some default value). F# has a specialized function for that: Seq.find
(or List.find
if host.AddressList
is an F# list, or Array.find
if host.AddressList
is an array. Those three functions take different input types but all work the same way conceptually, so from now on I'll focus on Seq.find
, which takes any IEnumerable
as input so is most likely to be what you need here).
If you look at the Seq.find
function's type signature in the F# docs, you'll see that it is:
('T -> bool) -> seq<'T> -> 'T
This means that the function takes two parameters, a 'T -> bool
and seq<'T>
and returns a 'T
. The 'T
syntax means "this is a generic type called T": in F#, the apostrophe means that what follows is the name of a generic type. The type 'T -> bool
means a function that takes a 'T
and returns a Boolean; i.e., a predicate that says "Yes, this matches what I'm looking for" or "No, keep looking". The second argument to Seq.find
is a seq<'T>
; seq
is F#'s shorter name for an IEnumerable
, so you can read this as IEnumerable<'T>
. And the result is an item of type 'T
.
Just from that function signature and name alone, you can guess what this does: it goes through the sequence of items and calls the predicate for each one; the first item for which the predicate returns true will be returned as the result of Seq.find
.
But wait! What if the item you're looking for isn't in the sequence at all? Then Seq.find
will throw an exception, which may not be the behavior you're looking for. Which is why the Seq.tryFind
function exists: its function signature looks just like Seq.find
, except for the return value: it returns 'T option
rather than 'T
. That means that you'll either get a result like Some "ip address"
or None
. In your case, you intend to return "?"
if the item isn't found. So you want to convert a value that's either Some "ip address
or None
to either "ip address"
(without the Some
) or "?"
. That is what the defaultArg
function is for: it takes a 'T option
, and a 'T
representing the default value to return if your value is None
, and it returns a plain 'T
.
So to sum up:
Seq.tryFind
takes a predicate function and a sequence, and returns a 'T option
. In your case, this will be a string option
defaultArg
takes a 'T option
and a default value, and returns a normal 'T
(in your case, a string
).With these two pieces, plus a predicate function you can write yourself, we can do what you're looking for.
One more note before I show you the code: you wrote let getIP : string = (code)
. It seems like you intended for getIP
to be a function, but you didn't give it any parameters. Writing let something = (code block)
will create a value by running the code block immediately (and just once) and then assigning its result to the name something
. Whereas writing let something() = (code block)
will create a function. It will not run the code block immediately, but it will instead run the code block every time the function is called. So I think you should have written let getIP() : string = (code)
.
Okay, so having explained all that, let's put this together to give you a getIP
function that actually works:
let getIP() = // No need to declare the return type, since F# can infer it
let isInternet ip = // This is the predicate function
// Note that this function isn't accessible outside of getIP()
ip.AddressFamily = AddressFamily.InterNetwork
let host = Dns.GetHostEntry(Dns.GetHostName())
let maybeIP = Seq.tryFind isInternet host.AddressList
defaultArg maybeIP "?"
I hope that's clear enough; if there's anything you don't understand, let me know and I'll try to explain further.
Edit: The above has one possible flaw: the fact that F# may not be able to infer the type of the ip
argument in isInternet
without an explicit type declaration. It's clear from the code that it needs to be some class with an .AddressFamily
property, but the F# compiler can't know (at this point in the code) which class you intend to pass to this predicate function. That's because the F# compiler is a single-pass compiler, that works its way through the code in a top-down, left-to-right order. To be able to infer the type of the ip
parameter, you might need to rearrange the code a little, as follows:
let getIP() = // No need to declare the return type, since F# can infer it
let host = Dns.GetHostEntry(Dns.GetHostName())
let maybeIP = host.AddressList |> Seq.tryFind (fun ip -> ip.AddressFamily = AddressFamily.InterNetwork)
defaultArg maybeIP "?"
This is actually more idiomatic F# anyway. When you have a predicate function being passed to Seq.tryFind
or other similar functions, the most common style in F# is to declare that predicate as an anonymous function using the fun
keyword; this works just like lambdas in C# (in C# that predicate would be ip => ip.AddressFamily == AddressFamily.InterNetwork
). And the other thing that's common is to use the |>
operator with things like Seq.tryFind
and others that take predicates. The |>
operator basically* takes the value that's before the |>
operator and passes it as the last parameter of the function that's after the operator. So foo |> Seq.tryFind (fun x -> xyz)
is just like writing Seq.tryFind (fun x -> xyz) foo
, except that foo
is the first thing you read in that line. And since foo
is the sequence that you're looking in, and fun x -> xyz
is how you're looking, that feels more natural: in English, you'd say "Please look in my closet for a green shirt", so the concept "closet" comes up before "green shirt". And in idiomatic F#, you'd write closet |> Seq.find (fun shirt -> shirt.Color = "green")
: again, the concept "closet" comes up before "green shirt".
With this version of the function, F# will encounter host.AddressList
before it encounters fun ip -> ...
, so it will know that the name ip
refers to one item in host.AddressList
. And since it knows the type of host.AddressList
, it will be able to infer the type of ip
.
* There's a lot more going on behind the scenes with the |>
operator, involving currying and partial application. But at a beginner level, just think of it as "puts a value at the end of a function's parameter list" and you'll have the right idea.
Upvotes: 8
Reputation: 359
In F# any if/else/then-statement must evaluate to the same type of value for all branches. Since you've omitted the else-branch of the expression, the compiler will infer it to return a value of type unit, effectively turning your if-expression into this:
if ip.AddressFamily = AddressFamily.InterNetwork then
ip.ToString() // value of type string
else
() // value of type unit
Scott Wlaschin explains this better than me on the excellent F# for fun and profit.
This should fix the current error, but still won't compile. You can solve this either by translating the C#-code more directly (using a mutable variable for the localIP value, and doing localIP <- ip.ToString()
in your if-clause, or you could look into a more idiomatic approach using something like Seq.tryFind.
Upvotes: 2