F # | Как управлять нулем (не обнуляемым) в выражении соответствия?

#f# #null #pattern-matching

#f# #null #сопоставление с образцом

Вопрос:

 open System.Linq

type Car(model:string, color:string) =
    member this.Model = model
    member this.Color = color
    
    member this.ToString() = sprintf "ModeL:%s Color:%s" model color
    
let cars = [
    Car("Ferrari", "red")
    Car("BMW", "blu")
]

let getCar model = 
    match cars.FirstOrDefault(fun c -> c.Model = model) with
    | car -> Some(car)                       // matches ALWAYS !
    //| car when car <> null -> Some(car)
    //| car when car <> (default(Object)) -> Some(car)
    //| null -> None
    //| Null -> None     

let mercedes = getCar("Mercedes")

let car = match mercedes with
          | Some c -> c.ToString()           // c is null !!!
          | _ -> "not found"
  

FirstOrDefault не возвращает значение с нулевым значением, поэтому я не могу сопоставить с null .
Итак, как проверить null возвращаемое из функции в выражении соответствия?

Я использую FirstOrDefault, потому что я пытаюсь использовать простейший объект (Seq) из перечисляемого.
Я знаю, что могу использовать что-то еще, начиная с перечислимого, но все же мне хотелось бы понять, чего мне здесь не хватает.

[Решение]

Благодаря предложению @Abel использовать .tryFind() я выполнил задачу, используя Seq.tryFind() которое возвращает Car option .

 let getCar model = 
    let cars = lazy(
       // this.Collection.Indexes.List().ToEnumerable()   // this is the real data I'm using (MongoDB indexes of a collection)
       // |> Seq.map parseIndex  // a function that create Car (Index) from the BsonDocumentBsonDocument
       cars.AsEnumerable()        
    )
    cars.Value |> Seq.tryFind(fun c -> c.Model = model)
    
let mercedes = match getCar("Mercedes") with
               | Some c -> c.ToString()
               | _ -> "not found"
         
let ferrari = match getCar("Ferrari") with
              | Some c -> c.ToString()
              | _ -> "not found"   
  

Ответ №1:

Классы в F # не могут иметь null в качестве надлежащего значения (это один из наиболее мощных аспектов F #). Однако вы можете разорвать этот контракт, добавив AllowNullLiteral атрибут:

 [<AllowNullLiteral>]
type Car(model:string, color:string) =
    member this.Model = model
    member this.Color = color
  

Теперь вы можете создавать экземпляры, которые являются null , и это будет проще использовать в вашем коде, когда вам нужно взаимодействовать с кодом, который может возвращать null .

Обратите внимание, что ваш код с | car -> является шаблоном переменной, что означает, что он улавливает все и присваивает значение переменной car . Не уверен, что вы хотите здесь сделать, но сопоставление шаблонов по классам не очень полезно.

Если вам нужно сопоставить для null , сделайте это первым совпадением, а второе совпадение может быть car , перехватывая все остальное. Тогда ваш код станет:

 [<AllowNullLiteral>]
type Car(model:string, color:string) =
    member this.Model = model
    member this.Color = color
    
    member this.ToString() = sprintf "ModeL:%s Color:%s" model color
    
module X = 
    let cars = [
        Car("Ferrari", "red")
        Car("BMW", "blu")
    ]

    let getCar model = 
        match cars.FirstOrDefault(fun c -> c.Model = model) with
        | null -> None
        | car -> Some(car)      // matches everything else
  

Еще одно замечание о вашем коде: Car тип с таким же успехом может быть создан как запись:

 type Car =
    { 
        Model: string
        Color: string
    }
  

И вместо использования LINQ более идиоматично использовать List.tryFind (или Seq.tryFind , если вы хотите использовать IEnumerable ) вместо этого, который автоматически возвращает параметр, и вам не нужно внезапно вводить null в свой код F #. Тогда ваш код в целом станет намного проще:

 type Car =
    { 
        Model: string
        Color: string
    }
    
    override this.ToString() = sprintf "ModeL:%s Color:%s" this.Model this.Color
    
module X = 
    let cars = [
        { Model = "Ferrari"; Color = "red" }
        { Model = "BMW"; Color = "blu" }
    ]

    let getCar model = cars |> List.tryFind (fun x -> x.Model = model)
  

Комментарии:

1. Вместо сопоставления с null и сопоставления с Option , вы могли бы использовать Option.ofObj , который возвращает None , если объект является null и Some в противном случае.

2. @rob.earwaker, я подумал об этом, и в целом я бы согласился, но здесь это не помогло бы сразу, потому что тип, принадлежащий F #, не поддерживается null , и вы получите ошибку компиляции с Option.ofObj . Это работает , если у вас есть тип CLI или вы добавили AllowNullLiteral , но это то, что вы можете добавлять только к классам, а не к записям / DU.