• Nie Znaleziono Wyników

Typy statyczne vs hierarchiczne vs obiekty

N/A
N/A
Protected

Academic year: 2022

Share "Typy statyczne vs hierarchiczne vs obiekty"

Copied!
13
0
0

Pełen tekst

(1)

Zagadnienie 6: Typy

System obliczeniowy Julii, tworzenie własnych struktur danych

Typy statyczne vs hierarchiczne vs obiekty

Problem: mamy pewne wyrażenie, powiedzmy

x + y

dla znanych typów/klasy x oraz y . Trzeba wygenerować kod asemblera służący do jego przeprowadzenia.

Podejście przez typy statyczne: sprawdzamy typy T1 wartości x oraz T2 wartości y . Szukamy na liście metod, czy istnieje + dla wartości typu T1 oraz T2 . Jeżeli istnieje, kompilujemy ją. Jeżeli nie, zwracamy błąd.

• brak narzutu obliczeniowego

• bardzo mała elastyczność, trudna obsługa złożonego kodu

W powyższym przypadku musi istnieć lista metod Float64 + Float64 , Int64 + Int64 , Int64 + Float64 , Float64 + Int64 itd. itp.

Zwykle języki zawierają elementy mające łagodzić ten minus, np. C/C++ zawiera szablony, nowe wersje również type traits.

Podejście obiektowe: Obiekt x wywołuje swoistą dla swojej klasy metodę + , która sprawdza klasę y , na tej podstawie znajduje właściwą procedurę i kompiluje ją.

• ustrukturyzowanie ułatwia obsługę złożonego kodu

• jeżeli jest to dynamiczne, narzut obliczeniowy

• brak symetrii ( x wywołuje y jest w kodzie czym innym niż y wywołuje x , mimo że operacja powinna być ta sama), związana z tym trudna obsługa funkcji wielu argumentów W powyższym przypadku możemy myśleć o ogólnej klasie Number , która zawiera przy sprawdzaniu dysponuje ogólnymi procedurami w rodzaju "jeżeli x lub y są Float64 , to zrzutuj tę drugą na Float64 i wywołaj Float64 + Float64 ).

Podejście przez typy hierarchiczne: sprawdzamy typy T1 wartości x oraz T2 wartości y . Szukamy na liście metod, czy istnieje + dla wartości typu T1 oraz T2 . Jeżeli istnieje, kompilujemy ją. Jeżeli nie istnieje skaczemy wyżej w drzewie typów i szukamy metody ponownie, aż znajdziemy lub dojdziemy do wierzchołka drzewa.

• brak narzutu obliczeniowego

(2)

• symetria, łatwa obsługa funkcji wielu argumentów

• mniejsze możliwości ustrukturyzowania kodu

W powyższym przypadku możemy mieć zapisane konkretne metody Float64 + Float64, Int64 + Int64 dla najprostszych przypadków, ale poza tym Number + Number czy Real + Real , które zajmują się bardziej złożonymi.

Kluczowa różnica między podejściem obiektowym a hierarchicznym:

W podejściu obiektowym metody są podczepione do klas, w przypadku typów hierarchicznych są od nich niezależne

Tworzenie typów

W Julii jako użytkownicy mamy dostęp do tworzenia typów na wszystkich poziomach.

Uwaga! Typy możemy dynamicznie tworzyć, ale nie możemy ich dynamicznie modyfikować.

Jeżeli chcemy zmienić definicję jakiegoś typu musimy zrestartować Julię.

Typy podstawowe

Definiujemy ciąg bitów zadanej długości oraz jego miejsce w drzewie typów który będzie miał własne metody przetwarzania. Składnia:

primitive type NazwaTypu <: Nadtyp n end

• n - ilość bitów

• Nadtyp - gdzie go podczepiamy, bez podania domyślnie to <: Any

Tak powstały typ nie ma żadnych metod poza operacjami bitowymi, trzeba je zapewnić samemu.

MethodError: no method matching MyInt72() Closest candidates are:

(::Type{T})(::AbstractChar) where T<:Union{AbstractChar, Number} at char.jl:

50

(::Type{T})(::Base.TwicePrecision) where T<:Number at twiceprecision.jl:243 (::Type{T})(::Complex) where T<:Real at complex.jl:37

...

Stacktrace:

[1] top-level scope @ In[3]:1

[2] eval

@ .\boot.jl:360 [inlined]

[3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::Strin g, filename::String)

In [1]: primitive type MyInt72 <: Integer 72 end

In [3]: x = MyInt72()

(3)

@ Base .\loading.jl:1094

5-element Vector{MyInt72}:

MyInt72(0x07000000001b899e01) MyInt72(0x000000000000000001) MyInt72(0x1c0000000000000089) MyInt72(0x9c000000000fe34c00) MyInt72(0x000000000000000020)

Typy abstrakcyjne

Defniujemy nazwę typu i jego pozycję w drzewie typów. Składnia jest podobna:

abstract type NazwaTypu <: Nadtyp end

Taki typ nie zawiera żadnej innej informacji, jest to etykieta porządkująca strukturę innych typów.

true

true

false

Integer

├─ Bool

├─ MyInt72

├─ MyInts

├─ Signed

│ ├─ BigInt

│ ├─ Int128

│ ├─ Int16

│ ├─ Int32

│ ├─ Int64

│ └─ Int8

└─ Unsigned ├─ UInt128

In [4]: x = Vector{MyInt72}(undef,5) # zaalokowane 5 zmiennych MyInt72, 5 x 72 bity

Out[4]:

In [6]: isabstracttype(Number)

Out[6]:

In [8]: isabstracttype(Real)

Out[8]:

In [10]: isabstracttype(Float64)

Out[10]:

In [11]: abstract type MyInts <: Integer end

In [13]: using AbstractTrees

AbstractTrees.children(T::Type) = subtypes(T) print_tree(Integer)

(4)

├─ UInt16 ├─ UInt32 ├─ UInt64 └─ UInt8

Integer

├─ Bool

├─ MyInt72

├─ MyInts

│ ├─ MyInt1

│ └─ MyInt2

├─ Signed

│ ├─ BigInt

│ ├─ Int128

│ ├─ Int16

│ ├─ Int32

│ ├─ Int64

│ └─ Int8

└─ Unsigned ├─ UInt128 ├─ UInt16 ├─ UInt32 ├─ UInt64 └─ UInt8

Unie typów

Wspólna etykieta na wartość mogącą być ze skończonej ilości typów Składnia: Union{T1,T2,...}

Julia automatycznie tworzy unie typów dla funkcji w prostych przypadkach.

O unii typów Union{A,B, ...} możemy myśleć jako o alternatywie w rodzaju

if typeof(x) == A ...

elseif typeof(x) == B ...

elseif ...

Różnica jest taka, że unia typów daje kompilatorowi pełną informację, co pozwala na lepszą optymalizację.

Są 2 szczególnie przydatne przypadki unii typów:

• Union{T,Nothing}

• Union{T,Missing}

Jest tylko jedna zmienna o typie Nothing , która nazywa się nothing , podobnie z missing . In [15]: abstract type MyInt1 <: MyInts end

abstract type MyInt2 <: MyInts end print_tree(Integer)

(5)

Obydwa reprezentują sytuację, gdy czegoś brakuje. Różnica:

• nothing - wynik działania funkcji, która nic nie zwraca

• missing - luki w ciągach danych rzeczywistych/symulacyjnych

MethodError: no method matching sin(::Nothing) Closest candidates are:

sin(::Float16) at math.jl:1159 sin(::ComplexF16) at math.jl:1160

sin(::Complex{T}) where T at complex.jl:831 ...

Stacktrace:

[1] top-level scope @ In[16]:2

[2] eval

@ .\boot.jl:360 [inlined]

[3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::Strin g, filename::String)

@ Base .\loading.jl:1094

missing

10-element Vector{Union{Missing, Float64}}:

1.0 missing missing 2.0 missing missing missing missing missing missing

10-element Vector{Union{Missing, Float64}}:

1.0 missing missing 4.0 missing missing In [16]: x = nothing

sin(x) # nie ma to sensu

In [18]: y = missing sin(y)

Out[18]:

In [19]: v = Vector{Union{Float64,Missing}}(missing,10) v[1] = 1.

v[4] = 2 v

Out[19]:

In [20]: v .^2

Out[20]:

(6)

missing missing missing missing

Przykład: kodowanie DNA

Chcemy zapisywać i przetwarzać informację zapisaną w DNA zapewniając, że pojedynczy kodon może przyjmować jedynie wartości A, C, T, G.

Proponowane rozwiązania:

1. Skorzystać z pakietu BioSequences .

2. Zapisać w postaci String lub Vector{Symbol} i ręcznie pilnować poprawności elementów.

3. Stworzyć BitArray{undef,2,n} i zapisywać wartości używając kolejnych kolumn.

4. Skorzystać z systemu typów Julii.

Omówimy 4.

Union{A, C, G, T}

(A(), C(), T(), G())

5-element Vector{Any}:

A() A() C() C() G()

10-element Vector{Union{A, C, G, T}}:

A() A() A() A() A() A() A()

In [21]: struct A end struct C end struct T end struct G end

Nucl = Union{A,C,T,G}

Out[21]:

In [22]: a, c, t, g = A(), C(), T(), G()

Out[22]:

In [25]: v1 = [a, a, c, c, g]

Out[25]:

In [26]: v = Vector{Nucl}(undef,10)

Out[26]:

(7)

A() A() A()

G()

0

Uwaga dotycząca wydajności

Tablice z komórkami wielu typów:

komórki typu abstrakcyjnego, np Vector{Integer} są niewydajne

komórki o uniach typów, np. Vector{Union{Bool,Int64}} są wydajne W razie wątpliwości można sprawdzić za pomocą komendy isabstracttype .

Komórki o uniach typów abstrakcyjnych, np. Matrix{Union{Float64,Real}} , też są niewydajne.

10000-element Vector{Float64}:

0.7358591599700428 0.014375881802169976 0.36556594584998403 0.4399182553595271 0.992018298370323 0.05026520919053867 0.16609389593603208 0.7757112142663602 0.4784123177504376 0.028798336481295816 0.8205618188524093 0.868224771022073 0.05188028745283835 ⋮ 0.6061537236056294 0.27537203134726385 0.4033707283819574 0.47696357791391475 0.2644580506518601 0.031017495180225785 0.5430989079895059 0.2346104160769107 0.22228266703684096 In [27]: v[1] = a

v[2] = c v[3] = g

Out[27]:

In [28]: v # może zawierać jedynie a lub c lub t lub g sizeof(v) # komórki nie zawierają nic

Out[28]:

In [31]: using BenchmarkTools

v = rand(10^4)

Out[31]:

(8)

0.37107225660752663 0.06743860490091813

10000-element Vector{Real}:

0.7358591599700428 0.014375881802169976 0.36556594584998403 0.4399182553595271 0.992018298370323 0.05026520919053867 0.16609389593603208 0.7757112142663602 0.4784123177504376 0.028798336481295816 0.8205618188524093 0.868224771022073 0.05188028745283835 ⋮

0.6061537236056294 0.27537203134726385 0.4033707283819574 0.47696357791391475 0.2644580506518601 0.031017495180225785 0.5430989079895059 0.2346104160769107 0.22228266703684096 0.37107225660752663 0.06743860490091813 0.7218154217943744

76.700 μs (5 allocations: 78.27 KiB) 10000-element Vector{Float64}:

0.6712242593484609 0.014375386639675606 0.3574779007502097 0.4258655050435514 0.8371316949129257 0.050244045258017615 0.16533127153258093 0.7002240047681856 0.4603703280274528 0.028794356024212476 0.7315289990529137 0.7631830184870745 0.05185701739707619 ⋮

0.5697106376007528 0.27190497049872553 0.392520770617107 0.45908375993850786 0.26138619846081407 0.03101252184162462 0.5167914790029935 0.23246409189969827 0.2204567015352551

In [34]: v2 = Vector{Real}(undef,10^4) # mniej informacji v2 .= v

Out[34]:

In [35]: @btime sin.(v)

Out[35]:

(9)

0.3626149180493014 0.06738749844985398

536.500 μs (19502 allocations: 383.12 KiB) 10000-element Vector{Float64}:

0.6712242593484609 0.014375386639675606 0.3574779007502097 0.4258655050435514 0.8371316949129257 0.050244045258017615 0.16533127153258093 0.7002240047681856 0.4603703280274528 0.028794356024212476 0.7315289990529137 0.7631830184870745 0.05185701739707619 ⋮ 0.5697106376007528 0.27190497049872553 0.392520770617107 0.45908375993850786 0.26138619846081407 0.03101252184162462 0.5167914790029935 0.23246409189969827 0.2204567015352551 0.3626149180493014 0.06738749844985398 0.6607484291422999

Typy złożone

Najbliższy odpowiednik klas w Julii. Podajemy nazwę typu, opcjonalnie nadtyp, oraz pola typu.

Składnia

Składnia definiowania:

struct NazwaTypu <: Nadtyp pole1::T1

pole2::T2 ...

end

Utworzone w ten sposób typy mają domyślny konstruktor

NazwaTypu(pole1::T1, pole2::T2, ...)

Dodatkowo utworzony zostaje też konstruktor pobierający inne typy argumentów o ile konwersja jest bezstratna.

In [36]: @btime sin.(v2)

Out[36]:

(10)

Inne konstruktory możemy tworzyć tak samo jak każdą inną metodę.

MyStr(42, 2.5, 'c')

Dla każdego typu dostęp do pól jest standardowy x.a, x.b, x.c itd.

42

2.5

'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

MyStr(1, 2.0, 'c')

InexactError: Int64(2.5) Stacktrace:

[1] Int64

@ .\float.jl:723 [inlined]

[2] convert

@ .\number.jl:7 [inlined]

[3] MyStr(x::Float64, y::Float64, c::Char) @ Main .\In[37]:2

[4] top-level scope @ In[43]:1

[5] eval

@ .\boot.jl:360 [inlined]

[6] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::Strin g, filename::String)

@ Base .\loading.jl:1094

Uwaga dotyczaca wydajności

In [37]: struct MyStr x::Int64 y::Float64 c::Char end

In [38]: str = MyStr(42,2.5,'c')

Out[38]:

In [39]: str.x

Out[39]:

In [40]: str.y

Out[40]:

In [41]: str.c

Out[41]:

In [42]: str2 = MyStr(1,2,'c') # 2 nie jest typu Float64, zrzutowane na Float64

Out[42]:

In [43]: str = MyStr(2.5,1.,'d') # nie jest jasne, czego oczekujemy po 2.5

(11)

Typy złożone o polach typu abstracyjnego są niewydajne.

Typ w rodzaju

struct X n::Integer end

jest (generalnie) złym pomysłem. Lepiej

struct X n::Int64 end

Jeżeli potrzebujemy większej elastyczności, używamy typów parametrycznych

Typy parametryczne

Typy parametryczne UnionAll znacząco uelastyczniają drzewo typów oraz ich obsługę. Jak sama nazwa wskazuje są to typy zależne od jednego lub więcej parametrów. Parametrami tymi mogą być inne typy lub wartości typu prostego ( Bool , Int64 , Char , itp.).

Składnia: nawiasy wąsate po nazwie typu, opcjonalnie nadtyp parametru

struct X{T}

...

end ogólniej

struct X{T <: S}

...

end

Powrót do przykładu z wyżej

struct X{T <: Integer}

n::T end

jest wydajny. W zależności od konkretnej potrzeby będziemy mieli do dyspozycji X{Int64}, X{Int32}, X{BigInt} itd. oraz metody przetwarzania.

In [44]: struct Point2D{T <: Real}

x::T y::T end

(12)

Point2D{Int64}(2, 3)

Point2D{Float64}(3.0, 4.5)

Point2D{BigFloat}(100.0, 2.0)

(100.0, 2.0)

Metody parametryczne

Możemy tworzyć metody, które będą obsługiwały ogólny zestaw przypadków dla typu parametrycznego. Składnia

metoda(argumenty) where T <: S

metoda(argumenty) where {T1 <: S1, T2 <: S2}

Jak zawsze domyślne jest <: Any .

Point2D{Int64}(2, 3)

Point2D{Int64}(3, 3)

Point2D{Int64}(5, 6) In [46]: x = Point2D(2,3)

Out[46]:

In [47]: y = Point2D(3.0,4.5)

Out[47]:

In [50]: z = Point2D(big"100.0",big"2.0")

Out[50]:

In [52]: z.x, z.y

Out[52]:

In [57]: Base.:+(p1::Point2D{T},p2::Point2D{T}) where T <: Real = Point2D(p1.x+p2.x,p2.

In [59]: x

Out[59]:

In [60]: x2 = Point2D(3,3)

Out[60]:

In [61]: x + x2

Out[61]:

In [62]: Base.:+(p1::Point2D{T},p2::Point2D{S}) where {T <: Real, S <: Real} = Point2D(

(13)

Point2D{Float64}(5.0, 7.5)

In [63]: x + y # Point{Int64} + Point{Float64}

Out[63]:

In [ ]:

Cytaty

Powiązane dokumenty

1 wprowadzono możliwość wniesienia skargi na uchwałę w sprawie wygaśnię- cia mandatu radnego (i to na początku tylko w przypadku przesłanki określonej w art. Wówczas dopiero

Rozpoczynamy od omówienia argumentów wysuwanych przeciwko „istnieniu&#34; logiki norm. Centralnym zagadnieniem logiki jest wynikanie. W dziedzinie zdań opisowych najprościej

uczenie się języka obcego jest zaliczane do kategorii uczenia się społecznego, gdyż (...) uczymy się go dzięki naśladow­ nictwu, obserwacji, a także wrodzonym predyspozycjom

25,407; 5,991; V = 0,446), można stwierdzić, że przy przyjętym poziomie istotności można odrzucić hipotezę o braku zależności pomiędzy badanymi cechami, oraz można

Miłość jest aktywną siłą w człowieku, siłą która przebija się przez mury oddzielające człowieka od jego bliźnich, siłę jednoczącą go z  innymi; dzięki

Karol Hube (syn znanego polskiego matematyka Jana Michała Hubego (1737–1807)) był w latach 1810–1833 profesorem matematyki wyższej w Uniwersytecie Jagiellońskim w

Przyzwyczailiśmy się do tego, że proces zapłodnienia jest losowy (na tym założe­ niu opierają się reguły genetyki mendlowskiej), tymczasem zdarzają się przypadki, że

Istotny jednak jest fakt, że znalazł się w nim fragment wiersza Podróż, zapowiadający rychły powrót do rodzinne- go domu — wówczas wobec zagłady dziecinnej arkadii