Trotz der Tatsache, dass dem Konzept von Julia die „klassische“ objektorientierte Programmierung mit Klassen und Methoden fehlt, bietet die Sprache Abstraktionswerkzeuge, eine Schlüsselrolle, die das Typensystem und die Elemente der funktionalen Programmierung spielen. Betrachten wir den zweiten Punkt genauer.
Das Funktionskonzept in Julia ist wahrscheinlich den Sprachen aus der Lisp-Familie (genauer gesagt den Lisp-1-Zweigen) am ähnlichsten, und Funktionen können auf drei Ebenen betrachtet werden: als Unterprogramme, als Abstraktionen zu einer bestimmten Abfolge von Aktionen und als Daten, die diese Abstraktion darstellen .
Stufe 1. Funktioniert als Routine
Die Zuweisung von Unterprogrammen und die Zuweisung eigener Namen erfolgt seit prähistorischen Zeiten, als Fortran als Hochsprache galt und C noch nicht da war.
In diesem Sinne sind Julia-Produkte Standard. "Feature" kann als die Tatsache bezeichnet werden, dass es syntaktisch keine Unterteilung in Prozeduren und Funktionen gibt. Unabhängig davon, ob das Unterprogramm aufgerufen wird, um einen Wert abzurufen oder nur eine Aktion für die Daten auszuführen, wird es als Funktion bezeichnet.
Die Definition einer Funktion beginnt mit der Schlüsselwortfunktion, gefolgt von einer Liste von Argumenten, einer Folge von Befehlen in Klammern, und das Wortede beendet die Definition:
""" sum_all(collection) Sum all elements of a collection and return the result """ function sum_all(collection) sum = 0 for item in collection sum += collection end sum end
Die Syntax unterscheidet sich durch das von Lisp geerbte Verhalten: Für eine "normale" Rückgabe eines Wertes aus einer Funktion ist die Wortrückgabe nicht erforderlich: Der Wert des letzten Ausdrucks, der vor der Rückgabe des
end
berechnet wurde. Im obigen Beispiel wird der Wert der variablen
sum
zurückgegeben. Somit kann
return
als Marker für ein bestimmtes Funktionsverhalten verwendet werden:
function safe_division(number, divisor) if divisor == 0 return 0 end number / divisor end
Für Funktionen mit einer kurzen Definition gibt es eine verkürzte Syntax ähnlich einer mathematischen Notation. Die Berechnung der Länge der Hypotenuse entlang der Länge der Beine kann also wie folgt definiert werden:
hypotenuse(a, b) = sqrt(a^2 + b^2)
Die "sichere" Unterteilung mit dem ternären Operator kann wie folgt geschrieben werden:
safe_division(number, divisor) = divisor == 0 ? 0 : number / divisor
Wie Sie sehen, müssen keine Typen für Funktionsargumente angegeben werden. Angesichts der Funktionsweise des Julia JIT-Compilers führt die Eingabe von Enten nicht immer zu einer schlechten Leistung.
Wie ich in einem
früheren Artikel zu demonstrieren versucht habe, kann der Julia-Compiler den Typ des Rückgabeergebnisses anhand der Typen der Eingabeargumente ableiten. Daher erfordert die Funktion
safe_division
beispielsweise nur minimale Änderungen für einen schnellen Betrieb:
function safe_division(number, divisor) if divisor == 0 return zero(number / divisor) end number / divisor end
Wenn nun die Typen beider Argumente in der Kompilierungsphase bekannt sind, wird seitdem auch der Typ des zurückgegebenen Ergebnisses eindeutig angezeigt Die
zero(x)
-Funktion gibt einen Nullwert des gleichen Typs wie ihr Argument zurück (und die Division durch Null hat gemäß
IEEE 754 einen darstellbaren Wert im Format von Gleitkommazahlen).
Funktionen können eine feste Anzahl von Positionsargumenten, Positionsargumente mit Standardwerten, benannte Argumente und eine variable Anzahl von Argumenten haben. Syntax:
Stufe 2. Funktioniert als Daten
Der Name der Funktion kann nicht nur bei direkten Aufrufen verwendet werden, sondern auch als Kennung, mit der die Prozedur zum Abrufen des Werts verknüpft ist. Zum Beispiel:
function f_x_x(fn, x) fn(x, x) end julia> f_x_x(+, 3) 6
Die "klassischen" Funktionen, die ein funktionales Argument annehmen, sind
map
,
reduce
und
filter
.
map(f, x...)
wendet die Funktion
f
auf die Werte aller Elemente aus
x
(oder Tupel von i-Elementen) an und gibt die Ergebnisse als neue Sammlung zurück:
julia> map(cos, [0, π/3, π/2, 2*π/3, π]) 5-element Array{Float64,1}: 1.0 0.5000000000000001 6.123233995736766e-17 -0.4999999999999998 -1.0 julia> map(+, (2, 3), (1, 1)) (3, 4)
reduce(f, x; init_val)
"reduziert" die Sammlung auf einen einzelnen Wert und "erweitert" die Kette
f(f(...f(f(init_val, x[1]), x[2])...), x[end])
:
function myreduce(fn, values, init_val) accum = init_val for x in values accum = fn(accum, x) end accum end
Da nicht wirklich bestimmt wird, in welcher Reihenfolge das Array während der Reduktion
fn(accum, x)
oder ob
fn(accum, x)
oder
fn(x, accum)
wird
fn(x, accum)
die Reduktion nur mit kommutativen oder assoziativen Operatoren wie der Addition ein vorhersagbares Ergebnis oder Multiplikation.
filter(predicate, x)
gibt ein Array von
x
Elementen zurück, die das Prädikatprädikat erfüllen:
julia> filter(isodd, 1:10) 5-element Array{Int64,1}: 1 3 5 7 9 julia> filter(iszero, [[0], 1, 0.0, 1:-1, 0im]) 4-element Array{Any,1}: [0] 0.0 1:0 0 + 0im
Die Verwendung von Funktionen höherer Ordnung für Operationen an Arrays anstelle des Schreibens einer Schleife bietet mehrere Vorteile:
- Der Code wird kürzer
map()
oder reduce()
zeigen die Semantik der ausgeführten Operation an. Dann müssen Sie noch die Semantik des Geschehens in der Schleife verstehenmap()
kann der Compiler verstehen, dass Operationen an Array-Elementen unabhängig von Daten sind, wodurch zusätzliche Optimierungen angewendet werden können
Level 3. Funktioniert als Abstraktionen
In
map()
oder
filter()
Sie häufig eine Funktion verwenden, der kein eigener Name zugewiesen wurde. Mit Julia können Sie in diesem Fall die
Abstraktion der Operationen für das Argument ausdrücken, ohne Ihren eigenen Namen für diese Sequenz einzugeben. Eine solche Abstraktion wird als
anonyme Funktion oder
Lambda-Funktion bezeichnet (da solche Funktionen in der mathematischen Tradition mit dem Buchstaben Lambda bezeichnet werden). Die Syntax für diese Ansicht lautet:
Sowohl benannte als auch anonyme Funktionen können Variablen zugewiesen und als Werte zurückgegeben werden:
julia> double_squared = x -> (2 * x)^2
Variabler Umfang und lexikalische Verschlüsse
Normalerweise versuchen sie, Funktionen so zu schreiben, dass alle für die Berechnung erforderlichen Daten durch formale Argumente erhalten werden, d. H. Alle im Hauptteil vorkommenden Variablennamen sind entweder die Namen formaler Argumente oder die Namen der Variablen, die im Funktionskörper eingeführt werden.
function normal(x, y) z = x + y x + y * z end function strange(x, y) x + y * z end
Über die
normal()
Funktion können wir sagen, dass in ihrem Körper alle Variablennamen
verwandt sind , d.h. Wenn wir überall (einschließlich der Argumentliste) "x" durch "m" (oder einen anderen Bezeichner), "y" durch "n" und "z" durch "sum_of_m_and_n" ersetzen, ändert sich die Bedeutung des Ausdrucks nicht. In der Funktion
strange()
ist der Name z
nicht verwandt , d.h. a) Die Bedeutung kann sich ändern, wenn dieser Name durch einen anderen ersetzt wird. b) Die Richtigkeit der Funktion hängt davon ab, ob zum Zeitpunkt des Aufrufs der Funktion eine Variable mit dem Namen „z“ definiert wurde.
Generell ist die
normal()
Funktion auch nicht so sauber:
- Was passiert, wenn eine Variable mit dem Namen z außerhalb der Funktion definiert wird?
- Die Zeichen + und * sind in der Tat auch nicht verwandte Bezeichner.
Mit Punkt 2 kann nichts anderes getan werden, als zuzustimmen - es ist logisch, dass Definitionen aller im System verwendeten Funktionen existieren müssen, und wir hoffen, dass ihre wahre Bedeutung unseren Erwartungen entspricht.
Punkt 1 ist weniger offensichtlich als es scheint. Tatsache ist, dass die Antwort davon abhängt, wo die Funktion definiert ist. Wenn es global definiert ist, ist
z
innerhalb von
normal()
eine lokale Variable, d.h. Selbst wenn es eine globale Variable
z
ihr Wert nicht überschrieben. Wenn sich die Definition der Funktion innerhalb des Codeblocks befindet, wird der Wert der externen Variablen geändert, wenn in diesem Block eine frühere Definition von
z
vorhanden ist.
Wenn der Funktionskörper den Namen einer externen Variablen enthält, wird dieser Name dem Wert zugeordnet, der in der Umgebung vorhanden war, in der die Funktion erstellt wurde. Wenn die Funktion selbst aus dieser Umgebung exportiert wird (z. B. wenn sie von einer anderen Funktion als Wert zurückgegeben wird), erfasst sie die Variable aus der internen Umgebung, auf die in der neuen Umgebung nicht mehr zugegriffen werden kann. Dies wird als lexikalischer Verschluss bezeichnet.
Abschlüsse sind hauptsächlich in zwei Situationen nützlich: Wenn Sie eine Funktion gemäß den angegebenen Parametern erstellen müssen und wenn Sie eine Funktion mit einem internen Status benötigen.
Betrachten Sie die Situation mit einer Funktion, die einen internen Zustand kapselt:
function f_with_counter(fn) call_count = 0 ncalls() = call_count
Fallstudie: alle gleichen Polynome
In einem
früheren Artikel wurde die Darstellung von Polynomen als Strukturen betrachtet. Insbesondere ist eine der Speicherstrukturen eine Liste von Koeffizienten, beginnend mit der jüngsten. Um das Polynom
p
am Punkt
x
zu berechnen
x
vorgeschlagen, die Funktion
evpoly(p, x)
, die das Polynom nach dem Horner-Schema berechnet.
Vollständiger Definitionscode abstract type AbstractPolynomial end """ Polynomial <: AbstractPolynomial Polynomials written in the canonical form --- Polynomial(v::T) where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) Construct a `Polynomial` from the list of the coefficients. The coefficients are assumed to go from power 0 in the ascending order. If an empty collection is provided, the constructor returns a zero polynomial. """ struct Polynomial<:AbstractPolynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) coeff = isempty(v) ? (0.0,) : tuple([Float64(x) for x in v]...) return new(length(coeff)-1, coeff) end end """ InterpPolynomial <: AbstractPolynomial Interpolation polynomials in Newton's form --- InterpPolynomial(xsample::Vector{<:Real}, fsample::Vector{<:Real}) Construct an `InterpPolynomial` from a vector of points `xsample` and corresponding function values `fsample`. All values in `xsample` must be distinct. """ struct InterpPolynomial<:AbstractPolynomial degree::Int xval::NTuple{N, Float64} where N coeff::NTuple{N, Float64} where N function InterpPolynomial(xsample::X, fsample::F) where {X<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}, F<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} if !allunique(xsample) throw(DomainError("Cannot interpolate with duplicate X points")) end N = length(xsample) if length(fsample) != N throw(DomainError("Lengths of X and F are not the same")) end coeff = [Float64(f) for f in fsample] for i = 2:N for j = 1:(i-1) coeff[i] = (coeff[j] - coeff[i]) / (xsample[j] - xsample[i]) end end new(N-1, ntuple(i -> Float64(xsample[i]), N), tuple(coeff...)) end end function InterpPolynomial(fn, xsample::T) where {T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} InterpPolynomial(xsample, map(fn, xsample)) end function evpoly(p::Polynomial, z::Real) ans = p.coeff[end] for idx = p.degree:-1:1 ans = p.coeff[idx] + z * ans end return ans end function evpoly(p::InterpPolynomial, z::Real) ans = p.coeff[p.degree+1] for idx = p.degree:-1:1 ans = ans * (z - p.xval[idx]) + p.coeff[idx] end return ans end function Base.:+(p1::Polynomial, p2::Polynomial)
Die Darstellung eines Polynoms in Form einer Struktur entspricht nicht vollständig seinem intuitiven Verständnis als mathematische Funktion. Durch Rückgabe des Funktionswerts können Polynome aber auch direkt als Funktionen angegeben werden. So war es:
struct Polynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}})
Wir transformieren diese Definition in eine Funktion, die ein Array / Tupel von Koeffizienten verwendet und die tatsächliche Funktion zurückgibt, die das Polynom berechnet:
function Polynomial_as_closure(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}})
Ebenso können Sie eine Funktion für das Interpolationspolynom schreiben.
Eine wichtige Frage: Gab es etwas, das in der neuen Definition in der vorherigen Definition verloren gegangen ist? Leider gab das Festlegen des Polynoms als Struktur Hinweise für den Compiler und für uns die Möglichkeit, arithmetische Operatoren für diese Struktur zu überladen. Leider bietet Julia keine Funktionen eines so leistungsfähigen Typsystems.
Glücklicherweise können wir in diesem Fall das Beste aus beiden Welten herausholen, da Julia es Ihnen ermöglicht, sogenannte aufrufbare Strukturen zu erstellen. Das heißt, Sie können ein Polynom als Struktur angeben, es aber auch als Funktion aufrufen! Zu den Definitionen der Strukturen aus dem vorherigen Artikel müssen Sie lediglich Folgendes hinzufügen:
function (p::Polynomial)(z::Real) evpoly(p, z) end function (p::InterpPolynomial)(z::Real) evpoly(p, z) end
Mit funktionalen Argumenten können Sie auch einen externen Konstruktor eines Interpolationspolynoms für eine bestimmte Funktion hinzufügen, die aus einer Reihe von Punkten aufgebaut ist:
function InterpPolynomial(fn, xsample::T) where {T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} InterpPolynomial(xsample, map(fn, xsample)) end
Wir überprüfen die Definition julia> psin = InterpPolynomial(sin, [0, π/6, π/2, 5*π/6, π])
Fazit
Die Möglichkeiten, die sich aus der funktionalen Programmierung in Julia ergeben, ergeben eine ausdrucksstärkere Sprache als ein rein imperativer Stil. Die Darstellung von Strukturen in Form von Funktionen ist eine bequemere und natürlichere Aufzeichnung mathematischer Konzepte.