Categories
Tech

F# Friday – The (Elusive) contains Function

Let’s say you have a set containing the nicknames of the guys in the band Rush, and you want to see whether a certain person is in the band. No problem:

let rush = set ["Dirk"; "Lerxst"; "Pratt"]

let gotAlex = rush |> Set.contains "Lerxst"
// val gotAlex : bool = true
let gotAngus = rush |> Set.contains "Angus"
// val gotAngus : bool = false

If, on the other hand, you have the same values in a list, things don’t go so well:

let rush = ["Dirk"; "Lerxst"; "Pratt"]

let gotAlex = rush |> List.contains "Lerxst"
//   rush |> List.contains "Lerxst"
//   -------------^^^^^^^^
//
// error FS0039: The value, constructor,
// namespace or type 'contains' is not defined

Huh? No contains function in the List module? OK, what if you try it with an array?

let rush = [|"Dirk"; "Lerxst"; "Pratt"|]

let gotAlex = rush |> Array.contains "Lerxst"
//   rush |> Array.contains "Lerxst"
//   --------------^^^^^^^^
//
// error FS0039: The value, constructor,
// namespace or type 'contains' is not defined

Still AOL! Don’t look for it in the Seq module either; it’s not there.

Lists, Arrays, and Sequences (Oh My!)

Well, there are two ways around these limitations. First, contains is just a special case of exists, which is defined in all the collections modules. As F#’s modules are open, it’s easy enough to add contains to your projects:

module List =
    let contains x = List.exists ((=) x)

module Array =
    let contains x = Array.exists ((=) x)

module Seq =
    let contains x = Seq.exists ((=) x)

And voila, all of the following now work fine:

let rushL = ["Dirk"; "Lerxst"; "Pratt"]
let rushA = [|"Dirk"; "Lerxst"; "Pratt"|]
let rushSeq = seq ["Dirk"; "Lerxst"; "Pratt"]

let gotAlex = rushL |> List.contains "Lerxst"
// val gotAlex : bool = true
let gotNeil = rushA |> Array.contains "Pratt"
// val gotNeil : bool = true
let gotGene = rushSeq |> Seq.contains "Gene"
//val gotGene : bool = false

// Lists and arrays are sequences!
let gotGeddy = rushL |> Seq.contains "Dirk"
// val gotGeddy : bool = true
let gotPeter = rushA |> Seq.contains "Peter"
// val gotPeter : bool = false

The other approach you could take is to use LINQ. Opening the System.Linq namespace grants you access to all of the Enumerable extension methods. That means that all of the following work:

open System.Linq

let rushL = ["Dirk"; "Lerxst"; "Pratt"]
let rushA = [|"Dirk"; "Lerxst"; "Pratt"|]
let rushSet = set ["Dirk"; "Lerxst"; "Pratt"]
let rushSeq = seq ["Dirk"; "Lerxst"; "Pratt"]

let gotAlex = rushL.Contains "Lerxst"
// val gotAlex : bool = true
let gotNeil = rushA.Contains "Pratt"
// val gotNeil : bool = true
let gotGeddy = rushSet.Contains "Dirk"
// val gotGeddy : bool = true
let gotGene = rushSeq.Contains "Gene"
//val gotGene : bool = false

If you prefer your F# to be a little more idiomatic, you can define new module functions that delegate to LINQ rather than using exists:

open System.Linq

module List =
    let contains x (l: 'T list) = l.Contains x

module Array =
    let contains x (a : 'T array) = a.Contains x

module Seq =
    let contains x (s : 'T seq) = s.Contains x

Strings

Similarly, the String module does not contain a contains function either. You can take the same tack as you did with the other collections and define contains in terms of exists:

module String =
    let contains x = String.exists ((=) x)

let gotX = "Lerxst" |> String.contains 'x'
// val gotX : bool = true
let gotA = "Lerxst" |> Seq.contains 'A'
// val gotA : bool = false

Or you likewise have the option of using LINQ:

open System.Linq

module String
    // String.Contains can take a char or a string,
    // so you have to specify which
    let contains (c : char) (s : string) = s.Contains c

let gotX = "Lerxst" |> String.contains 'x'
// val gotX : bool = true
let gotA = "Lerxst" |> Seq.contains 'A'
// val gotA : bool = false

Of course, because String.Contains is overloaded to take either a string or a char, you have a little more flexibility if you call it as a method on the string rather than an F# function:

"Pratt".Contains("tt")
// val it : bool = true
"Pratt".Contains('p')
// val it : bool = false

Maps

Finally, the Map module does have a containsKey function …

let rushM = ["Dirk", "bass"; "Lerxst", "guitar"; "Pratt", "drums"]
            |> Map.ofList
let gotGed = rushM |> Map.containsKey "Dirk"
// val gotGed : bool = true

… but no containsValue or contains functions:

rushM |> Map.contains "Dirk"
//   rushM |> Map.contains "Dirk"
//   -------------^^^^^^^^
//
// stdin(5,14): error FS0039: The value, constructor,
// namespace or type 'contains' is not defined

rushM |> Map.contains ("Dirk", "bass")

//   rushM |> Map.contains ("Dirk", "bass")
//   -------------^^^^^^^^
// 
// stdin(7,14): error FS0039: The value, constructor,
// namespace or type 'contains' is not defined

rushM |> Map.containsValue "bass"
//   rushM |> Map.containsValue "bass"
//   -------------^^^^^^^^^^^^^
// 
// stdin(9,14): error FS0039: The value, constructor,
// namespace or type 'containsValue' is not defined

As with the other collections, we can define contains and containsValue in terms of exists:

module Map =
    let contains (k,v) = Map.exists (fun x y -> (x,y) = (k,v))
    let containsValue v = Map.exists (fun _ x -> x = v)

let gotDirkOnBass = rushM |> Map.contains ("Dirk", "bass")
// val gotDirkOnBass : bool = true
let gotBassPlayer = rushM |> Map.containsValue "bass"
// val gotBassPlayer : bool = true

You could alternatively define contains in terms of LINQ, but it is more verbose and provides no advantage over Map.exists. Because neither LINQ nor the IDictionary interface provide a ContainsValue method, defining Map.containsValue is certainly more verbose and a little more involved than simply using Map.exists.

NOTE: As of this writing, F# 3.1 is the latest version. According to UserVoice, F# 4.0 adds the missing contains function to the List, Array, and Seq modules, along with a few other useful functions.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.