Wymagająca deklaracji typu w Julii

16

Czy jest jakikolwiek sposób na jawne wymaganie w Julii (np. Powiedzenie w module lub pakiecie), że typy muszą być deklarowane ? Czy na przykład PackageCompilerczy Lint.jlma żadnego wsparcia dla takich kontroli? Mówiąc szerzej, czy sama standardowa dystrybucja Julii zapewnia jakiś statyczny analizator kodu lub równoważny, który mógłby pomóc w sprawdzeniu tego wymogu?

Jako motywujący przykład, powiedzmy, że chcemy się upewnić, że nasza rosnąca baza kodu produkcyjnego akceptuje tylko kod, który zawsze jest deklarowany typu , zgodnie z hipotezą, że duże bazy kodu z deklaracjami typów są łatwiejsze w utrzymaniu.

Jeśli chcemy egzekwować ten warunek, czy Julia w swojej standardowej dystrybucji zapewnia mechanizmy wymagające deklaracji typu lub pomaga osiągnąć ten cel? (np. cokolwiek, co można sprawdzić za pomocą włókien, haczyków zatwierdzających lub równoważnych?)

Amelio Vazquez-Reina
źródło
1
nie jestem pewien, jak bardzo to pomaga, ale, podobnie jak myśli Bogumila, hasmethod(f, (Any,) )powróci, falsejeśli nie zdefiniowano żadnego generycznego. Nadal będziesz musiał dopasować liczbę argumentów (tj. hasmethod(f, (Any,Any) )Dla funkcji dwuargumentowej).
Tasos Papastylianou

Odpowiedzi:

9

Krótka odpowiedź brzmi: nie, obecnie nie ma narzędzi do sprawdzania typu kodu Julia. Zasadniczo jest to jednak możliwe i w przeszłości wykonano pewne prace w tym kierunku, ale obecnie nie ma na to dobrego sposobu.

Dłuższą odpowiedzią jest to, że „adnotacje typu” są tutaj czerwonym śledziem, tak naprawdę chcesz sprawdzania typu, więc szersza część pytania jest właściwie właściwym pytaniem. Mogę trochę porozmawiać o tym, dlaczego adnotacje tekstowe są czerwonym śledziem, o innych rzeczach, które nie są właściwym rozwiązaniem, i jak wyglądałoby właściwe rozwiązanie.

Wymaganie adnotacji typu prawdopodobnie nie pozwala osiągnąć zamierzonego celu: można po prostu wstawić ::Anydowolne pole, argument lub wyrażenie i mieć adnotację typu, ale nie taką, która mówi tobie lub kompilatorowi coś użytecznego na temat rzeczywistego typu tej rzeczy. Dodaje dużo szumu wizualnego bez dodawania jakichkolwiek informacji.

Co z wymaganiem konkretnych adnotacji typu? To wyklucza po prostu zakładanie ::Anywszystkiego (co i tak Julia domyślnie robi). Istnieje jednak wiele doskonale uzasadnionych zastosowań typów abstrakcyjnych, które spowodowałyby niezgodność z prawem. Na przykład definicja identityfunkcji to

identity(x) = x

Jaką adnotację na konkretny typ zastosowałbyś w xramach tego wymogu? Definicja ma zastosowanie do każdego x, niezależnie od typu - taki jest punkt funkcji. Jedyną poprawną adnotacją typu jest x::Any. To nie jest anomalia: istnieje wiele definicji funkcji, które wymagają poprawnych typów abstrakcyjnych, więc zmuszanie ich do używania konkretnych typów byłoby dość ograniczające pod względem rodzaju kodu Julii, który można napisać.

W Julii często mówi się o „stabilności typu”. Termin wydaje się pochodzić ze społeczności Julii, ale został wybrany przez inne dynamiczne społeczności językowe, takie jak R. Jest to trochę trudne do zdefiniowania, ale z grubsza oznacza, że ​​jeśli znasz konkretne typy argumentów metody, znasz również typ jego wartości zwracanej. Nawet jeśli metoda jest stabilna typu, to nie wystarczy, aby zagwarantować, że sprawdziłaby typ, ponieważ stabilność typu nie mówi o żadnych regułach decydujących o tym, czy coś ma sprawdzić. Ale zmierza to we właściwym kierunku: chcesz mieć możliwość sprawdzenia, czy każda definicja metody jest stabilna pod względem typu.

Wielu z was nie chce wymagać stabilności tekstu, nawet gdybyście mogli. Od Julii 1.0 powszechne jest używanie małych związków. Rozpoczęło się to od przeprojektowania protokołu iteracji, który teraz używa nothingdo wskazania, że ​​iteracja została wykonana, zamiast zwracania (value, state)krotki, gdy istnieje więcej wartości do iteracji. Te find*funkcje biblioteki standardowej również używać wartości zwracanej nothing, aby wskazać, że żadna wartość nie została odnaleziona. Są to technicznie niestabilności typu, ale są one celowe, a kompilator jest całkiem dobry w uzasadnianiu ich optymalizacji na podstawie niestabilności. Więc przynajmniej małe związki prawdopodobnie muszą być dozwolone w kodzie. Ponadto nie ma wyraźnego miejsca na narysowanie linii. Chociaż może można powiedzieć, że typ zwrotuUnion{Nothing, T} jest do zaakceptowania, ale nic bardziej nieprzewidywalnego niż to.

Tym, czego prawdopodobnie naprawdę nie chcesz, zamiast wymagać adnotacji lub stabilności typu, jest posiadanie narzędzia, które sprawdzi, czy Twój kod nie może generować błędów metod, a może szerzej, że nie spowoduje żadnego nieoczekiwanego błędu. Kompilator często może dokładnie określić, która metoda zostanie wywołana w każdej witrynie wywołującej, lub przynajmniej zawęzić ją do kilku metod. W ten sposób generuje szybki kod - pełna dynamiczna wysyłka jest bardzo powolna (na przykład znacznie wolniejsza niż vtables w C ++). Z drugiej strony, jeśli napisałeś niepoprawny kod, kompilator może emitować bezwarunkowy błąd: kompilator wie, że popełniłeś błąd, ale nie mówi ci do czasu wykonania, ponieważ są to semantyka języka. Można wymagać, aby kompilator mógł określić, które metody mogą być wywoływane w każdej witrynie wywoływania: to gwarantuje, że kod będzie szybki i że nie będzie żadnych błędów metod. To powinno zrobić dobre narzędzie do sprawdzania typów dla Julii. Jest to świetny fundament dla tego rodzaju rzeczy, ponieważ kompilator wykonuje już większość tej pracy w ramach procesu generowania kodu.

Stefan Karpiński
źródło
12

To interesujące pytanie. Kluczowym pytaniem jest to, co definiujemy jako deklarowany typ . Jeśli masz na myśli, że ::SomeTypew każdej definicji metody znajduje się instrukcja, jest to nieco trudne, ponieważ masz różne możliwości dynamicznego generowania kodu w Julii. Być może istnieje kompletne rozwiązanie w tym sensie, ale nie wiem (chciałbym się tego nauczyć).

Rzecz, która przychodzi mi do głowy, wydaje się stosunkowo prostsza do wykonania, to sprawdzenie, czy jakakolwiek metoda zdefiniowana w module akceptuje Anyjako argument. Jest to podobne, ale nie równoważne z wcześniejszym stwierdzeniem, ponieważ:

julia> z1(x::Any) = 1
z1 (generic function with 1 method)

julia> z2(x) = 1
z2 (generic function with 1 method)

julia> methods(z1)
# 1 method for generic function "z1":
[1] z1(x) in Main at REPL[1]:1

julia> methods(z2)
# 1 method for generic function "z2":
[1] z2(x) in Main at REPL[2]:1

wyglądają tak samo dla methodsfunkcji, jak podpis obu funkcji przyjmuje xjako Any.

Teraz, aby sprawdzić, czy jakakolwiek metoda w module / pakiecie akceptuje Anyjako argument dowolnej z zdefiniowanych w niej metod, można użyć czegoś takiego jak następujący kod (nie testowałem go dokładnie, ponieważ właśnie go zapisałem, ale wydaje się, że jest to głównie możliwe przypadki):

function check_declared(m::Module, f::Function)
    for mf in methods(f).ms
        if mf.module == m
            if mf.sig isa UnionAll
                b = mf.sig.body
            else
                b = mf.sig
            end
            x = getfield(b, 3)
            for i in 2:length(x)
                if x[i] == Any
                    println(mf)
                    break
                end
            end
        end
    end
end

function check_declared(m::Module)
    for n in names(m)
        try
            f = m.eval(n)
            if f isa Function
                check_declared(m, f)
            end
        catch
            # modules sometimes return names that cannot be evaluated in their scope
        end
    end
end

Teraz, gdy uruchomisz go na Base.Iteratorsmodule, otrzymasz:

julia> check_declared(Iterators)
cycle(xs) in Base.Iterators at iterators.jl:672
drop(xs, n::Integer) in Base.Iterators at iterators.jl:628
enumerate(iter) in Base.Iterators at iterators.jl:133
flatten(itr) in Base.Iterators at iterators.jl:869
repeated(x) in Base.Iterators at iterators.jl:694
repeated(x, n::Integer) in Base.Iterators at iterators.jl:714
rest(itr::Base.Iterators.Rest, state) in Base.Iterators at iterators.jl:465
rest(itr) in Base.Iterators at iterators.jl:466
rest(itr, state) in Base.Iterators at iterators.jl:464
take(xs, n::Integer) in Base.Iterators at iterators.jl:572

a gdy np. sprawdzisz pakiet DataStructures.jl, otrzymasz:

julia> check_declared(DataStructures)
compare(c::DataStructures.LessThan, x, y) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps.jl:66
compare(c::DataStructures.GreaterThan, x, y) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps.jl:67
cons(h, t::LinkedList{T}) where T in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\list.jl:13
dec!(ct::Accumulator, x, a::Number) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:86
dequeue!(pq::PriorityQueue, key) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\priorityqueue.jl:288
dequeue_pair!(pq::PriorityQueue, key) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\priorityqueue.jl:328
enqueue!(s::Queue, x) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\queue.jl:28
findkey(t::DataStructures.BalancedTree23, k) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\balanced_tree.jl:277
findkey(m::SortedDict, k_) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\sorted_dict.jl:245
findkey(m::SortedSet, k_) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\sorted_set.jl:91
heappush!(xs::AbstractArray, x) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps\arrays_as_heaps.jl:71
heappush!(xs::AbstractArray, x, o::Base.Order.Ordering) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps\arrays_as_heaps.jl:71
inc!(ct::Accumulator, x, a::Number) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:68
incdec!(ft::FenwickTree{T}, left::Integer, right::Integer, val) where T in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\fenwick.jl:64
nil(T) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\list.jl:15
nlargest(acc::Accumulator, n) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:161
nsmallest(acc::Accumulator, n) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:175
reset!(ct::Accumulator{#s14,V} where #s14, x) where V in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:131
searchequalrange(m::SortedMultiDict, k_) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\sorted_multi_dict.jl:226
searchsortedafter(m::Union{SortedDict, SortedMultiDict, SortedSet}, k_) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\tokens2.jl:154
sizehint!(d::RobinDict, newsz) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\robin_dict.jl:231
update!(h::MutableBinaryHeap{T,Comp} where Comp, i::Int64, v) where T in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps\mutable_binary_heap.jl:250

To, co proponuję, nie jest pełnym rozwiązaniem twojego pytania, ale uznałem je za przydatne dla siebie, więc pomyślałem o podzieleniu się nim.

EDYTOWAĆ

Powyższy kod zgadza fsię Functiontylko. Zasadniczo możesz mieć typy, które można wywoływać. Następnie check_declared(m::Module, f::Function)podpis mógłby zostać zmieniony na check_declared(m::Module, f)(właściwie wtedy sama funkcja pozwoliłaby Anyjako drugi argument :)) i przekazałaby wszystkie ocenione nazwy do tej funkcji. Następnie musisz sprawdzić, czy w funkcji methods(f)jest dodatni length(tak jak w methodsprzypadku braku wywołania zwraca wartość o długości 0).

Bogumił Kamiński
źródło