2017-07-03 16 views
9

Wpadłem ostatnio na nieoczekiwane zachowanie z kompilatora F #. Udało mi się znaleźć obejście, ale pierwotne zachowanie zbija mnie z tropu i chciałem sprawdzić, czy ktoś może mi pomóc zrozumieć, co to powoduje.Dlaczego kompilator F # czasami nieprawidłowo generalizuje funkcje?

Funkcja, którą zdefiniowałem jako nietypową, stawała się ogólna, co zakłócało zdolność funkcji do dzielenia stanu między wieloma wywołaniami. I uproszczone mojego przypadków użycia w dół do następujących:

let nextId = 
    let mutable i = 0 
    let help (key:obj) = 
    i <- i + 1 
    i 
    help 
nextId "a" // returns 1 
nextId "b" // also returns 1!!!! 

Dlaczego NextID typu „a -> int zamiast obj -> int? Oczywiście uogólnienie jest również odpowiedzialne za błąd, w którym wielokrotnie powraca 1, ale dlaczego w ogóle występuje uogólnienie?

Zauważ, że jeśli I zdefiniować ją bez nazywania funkcji zagnieżdżonych, że działa zgodnie z oczekiwaniami w dawaniu unikalnych identyfikatorów:

let nextId = 
    let mutable i = 0 
    fun (key:obj) -> 
    i <- i + 1 
    i 
nextId "a" // returns 1 
nextId "b" // returns 2 

ale jeszcze bardziej tajemnicza, z tą definicją, F # Interactive nie może zdecydować, czy NextID jest an (obj -> int) lub ('a -> int). Kiedy pierwszy raz zdefiniować go uzyskać

val NextID: (obj -> int)

ale jeśli po prostu eval

nextId 

uzyskać

val, to: (” a -> int)

Co tu się dzieje i dlaczego moja prosta funkcja staje się automatycznie ogólna zeded?

Odpowiedz

8

Zgadzam się, że jest to dość nieoczekiwane zachowanie. Myślę, że powodem, dla którego F # wykonuje uogólnienie, jest to, że traktuje on help (po zwrocie) jako fun x -> help x. Wywołanie funkcji, która zajmuje obj wydaje się być jednym przypadkiem, w którym kompilator wykonuje generalizację (ponieważ wie, że wszystko może być obj). To samo dzieje się uogólnienie, na przykład, w:

let foo (o:obj) = 1 
let g = fun z -> foo z 

Tutaj, g staje 'a -> int też, podobnie jak w pierwszej wersji. Nie do końca wiem, dlaczego robi to kompilator, ale to, co widzisz, można wytłumaczyć: 1) traktowaniem help jako fun x -> help x i 2) generalizowaniem wywołań przyjmujących obj.

Inną rzeczą, która się dzieje jest to, w jaki sposób F # traktuje ogólne wartości - ogólne wartości są generalnie problematyczne w językach ML (na tym polega cała sprawa "ograniczenia wartości"), ale F # pozwala na to w niektórych ograniczonych przypadkach - możesz na przykład napisać:

let empty = [] 

ta określa ogólny wartość typu 'a list. Ograniczeniem jest to, że kompiluje się to jako funkcję, która jest wywoływana za każdym razem, gdy uzyskujesz dostęp do wartości empty. Myślę, że twoja pierwsza funkcja nextId jest kompilowana w ten sam sposób - więc ciało jest oceniane za każdym razem, gdy uzyskujesz do niego dostęp.

To prawdopodobnie nie odpowiada na dlaczego, ale mam nadzieję, że dostarcza więcej wskazówek na temat tego, jak to się dzieje - iw jakich innych przypadkach zachowanie, które widzisz, może być rozsądne!

5

nie mogę powiedzieć dlaczego kompilator zdecyduje się uogólnić w pierwszym scenariuszu, ale ostatecznie rozróżnienie między nextId istoty typu obj -> int vs 'a -> int co napędza pozornie dziwne zachowanie tutaj.

Na co warto, można „siła” oczekiwane zachowanie w swoim pierwszym scenariuszu z jeszcze innego typu adnotacji:

let nextId : obj -> int = 
    let mutable i = 0 
    let help (key:obj) = 
     i <- i + 1 
     i 
    help 

Teraz, jeśli umieścić te wartości w modułach (jak w tym gist), kompilowania i sprawdzić zespół w ILSpy, przekonasz się, że kod jest prawie identyczne, z wyjątkiem gdzie komórka ref licznika jest tworzony:

  • w konkretnym przypadku nextId jest właściwością otrzymując funkcję, która jest inst antiated razem z komórką ref w statycznego inicjatora dla modułu, czyli wszystkich wywołań nextId akcję samo licznika,

  • W ogólnym przypadku nextId jest rodzajowy funkcja otrzymując funkcję, a komórka ref jest tworzony wewnątrz jego ciało, tzn. masz licznik na połączenie z nextId.

Więc kod emitowane w ogólnym przypadku może być faktycznie świadczone w F # z tym fragmencie:

let nextId() = 
    let mutable i = 0 
    fun key -> 
     i <- i + 1 
     i 

Najważniejsze jest to, że to ma sens, aby emitować ostrzeżenie kompilatora, gdy masz ogólna wartość taka jak ta. Łatwo uniknąć problemu, gdy już się o tym dowiesz, ale jest to jedna z tych rzeczy, których nie zobaczysz inaczej.