Determine if any kind of list, sequence, array, or IEnumerable is empty
Asked Answered
T

1

5

I am writing a Xamarin.Forms app using XAML for my views, and I am trying to write an IValueConverter whose job should be returning false if the input is "empty" for types where those semantics make sense (strings/lists/sequences/arrays/IEnumerables). I have started with the following, which returns false for empty strings, but I can't figure out how to extend this to lists, sequences, arrays, and IEnumerables:

type FalseIfEmptyConverter() =
  interface IValueConverter with 
    member __.Convert(value:obj, _, _, _) = 
      match value with
      | :? string as s -> (s <> "" && not (isNull s)) |> box
      // TODO: extend to enumerables
      | x -> invalidOp <| "unsupported type " + x.GetType().FullName

    member __.ConvertBack(_, _, _, _) =
      raise <| System.NotImplementedException()

Things I've tried that don't work:

  • :? list<_> does not match a (boxed) list (at least not of ints) and produces a warning This construct causes code to be less generic than indicated by its type annotations. The type variable implied by the use of a '#', '_' or other type annotation at or near [...] has been constrained to be type 'obj'
  • :? list<obj> does not produce the warning, but also doesn't match a boxed list of ints
  • It's the same with :? seq<_> and :? seq<obj>
  • It's the same with :? System.Collections.Generic.IEnumerable<obj> and IEnumerable<_> (and if I place it below a similar seq match as given above, it warns that the rule will never be matched, which makes sense since AFAIK seq corresponds to IEnumerable)
Thong answered 14/11, 2017 at 8:5 Comment(1)
match value with | :? System.Collections.IEnumerable as s -> s.GetEnumerator().MoveNext() |> not | x -> invalidOp <| "unsupported type " + x.GetType().FullNameSymmetry
J
6

Using Foggy Finder's idea to use the non-generic IEnumerable:

let isEmpty (x:obj) =
    match x with
    | null -> true
    | :? System.Collections.IEnumerable as xs -> xs |> Seq.cast |> Seq.isEmpty
    | _ -> invalidOp <| "unsupported type " + x.GetType().FullName

isEmpty "" // true
isEmpty [] // true
isEmpty (set []) // true
isEmpty [||] // true
isEmpty null // true

isEmpty "a" // false
isEmpty [|1|] // false

isEmpty 1 // exception

All of the types that you want to test are sub-types of Seq<'a>, which is exactly the same as IEnumerable<'a> (including string, which is a seq<char>). But this is also sub-type of a non-generic type called IEnumerable (note the lack of type parameter). This is similar to an IEnumerable<obj>, where every item has been boxed. This is why we can cast all of these to IEnumerable, then use Seq.cast to convert that into IEnumerable<obj> so that we can use Seq.empty, which only works on the generic type.

Judgemade answered 14/11, 2017 at 8:54 Comment(3)
Works wonderfully, thanks! But I don't really understand why must use IEnumerable instead of IEnumerable<obj>. Could you elaborate a bit?Thong
When doing a type test, a string is a seq<char>, and a char is an obj, but a string is not a seq<obj>. The check for ancestor types does not extend down to the type parameters, even though it could in theory. I don't know why F# behaves that way or if it is actually because of .NET, or if there is a fundamental difficulty here vs just a lack of the feature.Judgemade
Thanks, that clears it up and is more or less what I suspected.Thong

© 2022 - 2024 — McMap. All rights reserved.