rss feed articles activities all_comments

indeedgeek.de

Florian Eitel

Programmbeschreibung auf Metaebene

Wieder Platz für Code

Halt, stop. Nicht so voreilig. Jetzt wird erst einmal Quicksort programmiert. Ja, los jetzt.

Fertig? Wie sieht es aus? Noch hin bekommen? Vergleichen wir mal mit meiner Lösung:


def quicksort list
  return [] if list.empty?
  return quicksort(list.select {|x| x < list.first}) +
    list.select {|x| x == list.first} +
    quicksort(list.select {|x| x > list.first})
end

puts quicksort([0,1,2,3,4,5,6,7,8].shuffle).inspect

Jaja, ich weiß, nicht gerade performant aber darum gehts jetzt nicht. Ein erster, brauchbarer Entwurf ist es allemal. Gerne schreibe ich Programme erst einmal schön und funktional, bevor ich mir dann Gedanken mache wie ich ich sie weiter optimiere. Dazu verwende ich gerne einen Kommentar wie TODO oder FIXME. Also fügen wir das mal in den Code ein. Schön wäre zudem noch eine Überprüfung ob der Parameter auch ein Array ist. Auch wäre eine kleine Optimierung die bei einem Element dieses schon zurück gibt statt sich noch einmal aufruft ist schnell eingebaut. Zudem hat die Erfahrung gezeigt Logging ist immer von Vorteil, Nebenläufigkeit ist recht einfach einbaubar, wir müssen das ganze noch Dokumentieren und Tests und …

Gut bauen wir mal ein (Komplettes Beispiel – Bitte nicht verwenden! Nur zur Demo ):


# Sort recursive
#
# Using first Element as pivot
# Preserve ordering
# for max 300 elements
# Cache last Items
#
# Open Issues:
# * Enumerable has no empty? method
#
# Open Tasks:
# * use Enumeration#partition method for performance
#
# Parameters:
#   list of type Enumerable
# Result:
#   sorted Array
def quicksort list, config
  config[:logger].debug "Entering quicksort(#{list.inspect})" if config[:log_enter_method]
  assert list.kind_of?(Enumerable), "need Enumerable as Parameter"
  raise :to_many_elements if list.count > 300

  return []   if list.empty?     # FIXME Enumerable hast no empty? method
  return list if list.count == 1 # Enumerable uses count!

  if has_cache_for? :quicksort, list
    config[:logger].debug "return value from cache" if config[:log_cache]
    return cached_value_for :quicksort, list
  end

  result = nil
  if list.size < 5 # saves recursion overhead
    config[:logger].info "optimize sort for #{list.size} elements" if config[:log_optimization]
    result = list.sort
  else
    less = more = nil
    if list.size > 50 # only fork threads if it is enough work to do
      config[:logger].info "creating future for #{list.size} elements" if config[:log_future_fork]
      less = Future.new do quicksort(list.select {|x| x < list.first}, config) end
      more = Future.new do quicksort(list.select {|x| x > list.first}, config) end
    else
      config[:logger].info "creating no future for #{list.size} elements" if config[:log_nofuture_fork]
      # TODO use Enumeration#partition method for performance
      less = quicksort(list.select {|x| x < list.first}, config)
      more = quicksort(list.select {|x| x > list.first}, config)
    end
    pivot = list.select {|x| x == list.first}
    result = less + pivot + more
  end

  assert result.count == list.count, "result has less items than input"
  assert result.kind_of?(Array), "result must be an Array"
  assert result.sort == result, "result must be sorted"

  config[:logger].debug "Leaving quicksort(#{result.inspect})" if config[:log_leave_method]
  update_cache_for :update, list, result
  return result
end

l = (1..200).to_a.shuffle
set_cache_size_for :quicksort, 300
puts quicksort(l, config).inspect

WTH? Wo ist unser Code hin? Erschlagen von zusätzlichen Informationen und Umbauten für Performance oder Nebenläufigkeit. Der eigentliche Code in seiner vorherigen Schönheit ist kaum noch erkennbar. Und doch machen die Änderungen Sinn.

Was haben wir überhaupt eingebaut? Neben der Funktionalität schreiben wir meist Dokumentation mit in die Sourcen. Fehlerüberprüfung, Asserts, Logging und Optimierungen sind auch ein wichtiger Bestandteil. Aber das Spektrum reicht noch viel weiter. Persistenz, Tests, Konfiguration, Authentifizierung, Nebenläufigkeit, … Die Anwendungsgebiete sind Vielfältig.

Das Problem dabei: Es hat nicht direkt mit der Funktionalität etwas zu tun. Die Informationen sind eher eine Art Zusatzfunktionalität, Beschreibungen oder viel mehr Metainformationen. Aber wie löst man nun den Zwiespalt zwischen Schönheit und zusätzlicher Beschreibung?

Programmierung in der Dokumentation

Oftmals wird hierbei der Weg über Dokumentation genommen. Bekanntestes Beispiel ist Javadoc:


/**
 * Checks if a language is usefull
 *
 * @param language Language to check
 * @return         true if not Java should be in lower case
 * @author Some Programming Star
 **/
public boolean isUsefull(String language) {
  if (language.equals("java")) return false;
  return true;
}

Hier wird viel Redundanz geschafft, die gerade dazu einlädt inkonsistent zu werden. Zudem werden die Anforderungen in der Dokumentation nicht im Code überprüft beziehungsweise die Funktion nicht brauchbar beschrieben. Einige ältere Frameworks kodieren auch oft Informationen wie Pre und Post-Conditions mit in die Kommentare und lassen diese über einen Preprozessor verarbeiten. Das löst zwar das Problem ein wenig, da ein bisschen Redundanz heraus genommen wird aber Programmierung in Kommentaren? Postprozessor? Das muss schöner gehen. (In Python gibt es zum Beispiel die Möglichkeit Tests in den Kommentaren zu schreiben).

Function Annotations

Eine andere Herangehensweise sind Function Annotations mit der Marker an eine Funktion geheftet wird die ein entsprechendes Verhalten herbeiführen. Ich finde das in der Python Welt sehr cool gelöst. Dort ist es so, dass die Funktion in der Annotation (in Python Decorator genannt) gewrapped wird:


def logging_decorator(f):
  def log(*x):
    print("call function %s with params %s" % (f.__name__, x))
    result = f(*x)
    print("call function %s result: %s" % (f.__name__, result))
    return result
  return log

@logging_decorator
def perimeter(radius):
  return 2 * math.pi * radius

Wird nun die Funktion perimeter definiert wird erst logging_decorator aufgerufen welche eine Funktion zurück gibt. Statt der Funktion perimeter wird nun eigentlich log instantiiert welche den Aufruf an das eigentliche perimerter weitergibt. Zuvor und anschließend wir allerdings gelogged. Auf diese Art hat man Logging sehr schön gekapselt. Ähnliche Ansätze gibt es auch in Sprachen wie Java (nur, dass es dort von Halbaffen eher zufällig ausgekotzt wurde).

Kleinteilige Unterscheidung

Allerdings trifft das noch nicht genau mein Wunsch. Ich finde es zum einen etwas störend, dass dies nur auf Methodenebene möglich ist. Manchmal möchte man ja auch Annotationen an kleinere Einheiten hängen. Schön wäre es wenn man einen Block wie in Ruby mittels begin ... end definieren kann und dem auch eine Annotation verpassen könnte. Oder mehr Ruby Style mit Blocks:


login(user, password)
require_admin(user) do
  delete_everything
end

Metabeschreibung

Zum anderen wrapped dies nur Funktionalität in anderer Funktionalität. Dies ist sicher schon mal nicht schlecht aber ich suche eher so etwas wie eine Metabeschreibung meines Codes.


Sorting = Class.new do |c|
  c.method(:bubblesort, :class_method => true, :deprecated => true) do |m|
    ...
  end
  c.method(:quicksort, :class_method => true) do |m|
    m.doc = "sorts list"
    m.param = Param.new(:name => list) do |p|
      p.doc = "list to sort"
      p.condition = do |x| (x.kind_of? Enumerable) end
      p.condition = Condition.new do |con|
        con.code { size <= 300 }
        con.task = "FIXME Enumerable has no attribute size"
      end
    end
    m.result       :doc => "sorted list",  :kind_of => Enumerable, :sorted => true, :size => :list.size
    m.logs         :debug => [:params, :result, :cache, :optimization]
    m.optimization :doc => "retuns empty list if empty", :result => [], :condition => list.empty?
    m.optimization :doc => "returns list if list only has one element", :result => list, :condition => (list.count == 1)
    m.cache        :entries => 300

    m.code(:condition => (list.size < 5), :doc => "saves recursion overhead") do return list.sort end

    m.code do |c|
      less = more = nil
      less = c.code(:parallel => {:condition => (list.size > config.fork_threshold)}) do |p|
        p.parallel.doc => "only fork threads if it is enough work to do"
        quicksort(list.select {|x| x < list.first})
      end
      more = c.code(:parallel => {:condition => (list.size > config.fork_threshold)}) do |p|
        quicksort(list.select {|x| x > list.first})
      end
      pivot = list.select {|x| x == list.first}
      return less + pivot + more
    end
  end
end

l = (1..200).to_a.shuffle
puts Sorting.quicksort(l).inspect

Bei dem Beispiel habe ich ein bisschen gespielt um zu probieren wie so etwas aussehen könnte. Ist an Ruby angelehnt, soll aber eher Pseudocode sein der verschiedene Ideen zeigt (daher verschiedene Syntax Ideen). Man könnte nun sagen, das ist nichts anderes als noch mehr Komplexität in die Sprache zu holen. Ich brauche ein neues Keyword was auch einfach mit einer If-Abfrage zu lösen wäre. Das hat auch seine Berechtigung aber dazu mehr Unten. Das Programm dokumentiert sich so aber fast selbst. Man kann sehr schön eine Dokumentation daraus generieren, die nicht gleich inkonsistent ist. Zudem betrachte ich auch kleinere Codeteile als Objekte und verpasse ihnen Eigenschaften und Funktionen.

Anwendung

Um zu verdeutlichen welche Vorteile der Code bietet ein paar Beispiele auf der Kommandozeile (ebenfalls nur Pseudocode):


>>> quicksort.doc
"sorts list"
>>> quicksort.param
[#>> quicksort.preconditions
nil
>>> quicksort.code
if list < 5 # saves recursion overhead
  return list.sort
else
  less = p.task do quicksort(list.select {|x| x < list.first}) end
  more = p.task do quicksort(list.select {|x| x > list.first}) end
  pivot = list.select {|x| x == list.first}
  return less + pivot + more
end
>>> quicksort.optimizations
[#, #]
>>> quicksort.call_with_tracking([1,4,3])
## logging :: :debug == false in config
## params:
### param(:list):
#### doc: "list to sort" :: pass
#### kind_of? Enumerable :: pass
#### size < 300          :: pass
## optimization
### "returns emtpy list if empty" :: not used
### "returns list if list only has one element" :: not used
## cache :: not found -> (adding)
## code
### list < 5 :: not used
## code
### not work parallel
....
>>> quicksort.tests
nil
>>> quicksort.tests.run
No tests Found

Ich denke damit wird Code Debugging extrem erleichtert und Dokumentation wird implizit. Tests könnten vereinfacht werden. Recht mächtig auch wenn man das gerade als nicht mehr als ein paar ungeordnete Hirngespenster von mir sehen darf.

Anwendung bei Python

Ein bisschen in die Richtung geht Python mit Function Annotations (nicht zu verwechseln mit den Descriptoren oben die ich als Annotationen benannt habe). Dort kann man Parametern und dem Return-Wert Tupel anhängen, die sich für alles mögliche nutzen lassen können. Hier ein Beispiel für Type-Checking mittels Descriptoren und Annotations:


def call_check(f):
  def check(*x):
    argcount = f.__code__.co_argcount
    assert len(x) == argcount, ("expected %d params - got %d" % (argcount, len(x)))
    for i in range(0, argcount):
      varname = f.__code__.co_varnames[i]
      expected_type = f.__annotations__[varname]
      assert type(x[i]) == expected_type, "param %s: expected %s - got %s" % (varname, expected_type, type(x[i]))
    return f(*x)
  return check

def calc_perimeter1(radius : int) -> float:
  """returns perimeter"""
  return 2 * math.pi * radius

assert calc_perimeter1.__annotations__ == {'radius' : int, "return" : float}
assert calc_perimeter1.__doc__ == "returns area"

@functools.lru_cache(maxsize=100)
@call_check
def calc_perimeter2(radius : int) -> float:
  return 2 * math.pi * radius

Man sieht daran auch sehr schön wie Python schon die Funktion als Objekt verwendet. So kann man die Dokumentation auf der Konsole zum Beispiel abfragen oder die Anzahl der Parameter und deren Namen.

Aspektorientierung

Auch wenn ich mit Aspektorientierung noch nicht so viel Erfahrung habe möchte ich es an dieser Stelle nicht unerwähnt lassen. Aspektorientierung versucht genau diese Problemstellung zu lösen. Dazu sind in der Sprache so genannte Join Points definiert in die man sich rein klinken kann. Dazu definiert man einen Pointcut der auf mehrere Join Points passt. Dies kann über eine Art Abfragesprache geschehen. Anschließend wird an diesen Pointcut eine Funktion gehängt die Advice genannt wird. Diese wird nun immer bei erreichen eines mittels Pointcut ausgewählten Joint Points ausgeführt.

So kann man Beispielsweise den Aufruf der Methode m als Joint Point sehen der mittels eines Pointcuts ausgewählt wird um dann das Advice Logging aufzurufen:


def m(x)
  return x*x
end

m(5) # <- Join Point ergibt sich durch den Aufruf

around_method_call(:m) do |x| # <- Selektiert alle aufrufe von m (Pointcut)
  puts "call m" # <- Für den Advice dann jedes mal aus
  r = m(x)
  puts "m called"
  return r
end

Was ich aber mal empfehlen kann ist sich die Wikipedia Seite zu Cross-Cutting Concern anzuschauen. Da wird mein Anliegen nochmal von einer anderen Sichtweise angegangen und strukturierter Beschrieben. Ganz wichtig dabei finde ich den einen Satz:

Die kostengünstige und termingerechte Entwicklung und Wartung qualitativ hochwertiger Software ist das Primärziel der Softwaretechnik. Um dieses Ziel zu erreichen, ist eine möglichst gut modularisierte Software mit einer möglichst geringen Komplexität notwendig. In einem konventionellen System, wobei hier auch die objektorientierten Ansätze hinzugehören, können Kernfunktionalitäten für sich allein betrachtet nach den Regeln der Kunst sauber in Module getrennt werden. Es gibt jedoch Anforderungen wie Fehlerbehandlung, Performance und Sicherheit in jedem System, die alle Kernfunktionalitäten betreffen und sich deshalb nicht eindeutig einem Software-Modul zuordnen lassen. Dies führt dazu, dass Fragmente solcher querschnittlicher Funktionen aufgrund fehlender Kohäsion nicht zugeordnet und ungekapselt im ganzen Code verstreut sind. Diese Anforderungen verhindern in konventionellen Software-Systemen eine saubere Modularisierung und beeinträchtigen Pflege, Verständlichkeit, Wiederverwendbarkeit und (Rück)-Verfolgbarkeit. Verantwortlich hierfür ist bei konventionellen Programmiersprachen die Systemdekomposition, die nur eine Dimension zulässt. In diesem Zusammenhang spricht man auch von dominanter Dekomposition, d.h. ein natürlicherweise mehrdimensionales Problem muss eindimensional gelöst werden.

Nochmal langsam: Man ordnet die Software nach bestimmten Kriterien in Pakete wie zum Beispiel Lagerhaltung oder GUI. Nun muss dort Logging implementiert werden was dazu führt, dass wir jede Methode anfassen müssen um dort Logging einzubinden. Das führt dazu, dass die Funktion Logging überall verstreut ist und nicht zentral auftritt. Dies verhindert natürlich die Punkte, die ich mit der Aufteilung erreichen wollte. Schult daran ist, dass in “konventionellen Programmiersprachen” (was auch immer das heißen mag) eine Systemdekomposition in nur eine Dimension zulässig ist. Das heißt wenn ich ein Stück Code habe ist es immer nur in einer Komponente, Modul oder was auch immer. Ein Codestück ist nicht im Paket Lagerhaltung und gleichzeitig im Paket Logging. Daher muss dieses mehrdimensionale Problem – also das Problem, dass meine GUI auch Logging hat – eindimensional gelöst werden indem ich die Eigenschaft Logging aufbreche und quer in die GUI verstreue.

Aspektorientierung versucht dem entgegen zu wirken indem man die Funktionalität Logging an einer Stelle definiert und dann über einen Pointcut in das Modul GUI einwebt wo es benötigt wird.

Diese Ansicht steht ein bisschen im Kontrast zu meiner Definition oben. Erst einmal der offensichtlichste Unterschied: Aspektorientierung webt Code in anderen ein. Mehr nicht. So kann man sagen vor jeder Methode soll Logging ausgeführt werden und dann wird dies zum Beispiel im Binary dort eingefügt. In meiner Idee versuche ich aber auch die Idee von Metainformationen einzubringen und diese auch zur Laufzeit abfragbar zu machen. Dabei geht es mir nicht nur um Funktionalität sondern auch um Dinge wie Dokumentation.

Ich dachte anfänglich, dass ich bei meiner Überlegung Joint Points und Pointcuts vermischt habe da ich explizit angebe hier kommt Logging rein und die ausführende Einheit dann ein wo anders definiertes Advice aufruft. Mittlerweile glaube ich, mein Gedanke ging etwas zu weit. Logging ist zum Beispiel nicht unbedingt eine Eigenschaft einer Methode daher ist es vielleicht als Property falsch angesiedelt. Es würde wirklich mehr Sinn machen Logging per Aspektorientierung an bestimmte Methoden zu heften. Properties sind eher eine Auswahlhilfe beim Selektieren was gelogged werden soll. Also beispielsweise immer wenn eine Optimierung greift oder alle Methoden mit dem Property :logthis.

Metabeschreibung

Aber zurück zu meinem Thema. Ich würde gerne unterscheiden zwischen der eigentlichen System Logik und Metainformationen. Dazu habe ich mir mal ein paar Gedanken gemacht was solche Metainformationen sein können:

Wie man schon sieht, es ergibt sich ein breites Spektrum. Und mit etwas Pech tritt alles gleichzeitig in einer Funktion auf. Ich habe oben in meinem Pseudo Code versucht mal die Teile herauszutrennen und explizit zu kennzeichnen. Aspektorientierung geht einen anderen Weg und definiert die Funktionalität außerhalb und stellt dann eine Anfrage die angibt wo die Funktionalität eingewebt werden soll. Beides hat finde ich seine Berechtigung und ergänzt sich gegenseitig. Ich definiere Eigenschaften von Codeblöcken und Aspektorientierung kann mit Hilfe dieser Teile auswählen und darauf Logik anwenden. Wichtig ist mir aber, dass das gesamte System reflektierbar bleibt und selbstbeschreibend bleibt. Ein Beispiel: Ich definiere das Property Pre-Condition, dokumentiere es und hinterlege eine Regel. Aspektorientierung fügt nun vor dem Methodenaufruf Code ein der die Regel auswertet. Auf der Kommandozeile kann ich aber das als Property der Methode anzeigen.

Fraglich ist nur die Implementierung der Funktion. Ich hatte oben direkt die Beschreibung direkt in dem Code abgebildet. Somit kann die Dokumentation dynamisch zur Laufzeit erzeugt werden. Dieses System ist sehr mächtig aber möchte man nur die Dokumentation auswerten oder statische Code-Analyse in einer IDE betreiben ist es sehr schwierig, da die Funktion erst ausgeführt werden muss bevor alle Informationen vorhanden sind. Anderes Beispiel sind Tests: Auch diese sind für mich Metabeschreibungen (besonders im Bezug auf BDD). Möchte man nur diese Ausführen möchte man nicht zuvor den Code selbst erst ausführen müssen. Daher wären verschiedene Abarbeitungsstränge sinnvoll. Bei Java kann man sich das als unterschiedliche main-Funktionen vorstellen. Führt man die eine aus wird nur die Dokumentation erstellt, bei der anderen nur Tests und bei einer dritten zum Beispiel die normale Funktionalität. Wichtig ist, dass diese vollständig voneinander separiert sind und keinen Einfluss aufeinander haben. Ich hatte das in meiner Syntax Idee oben noch nicht wirklich berücksichtigt. Meine Überlegung war von einer Methode nur gewisse Teile zu evaluieren. Also zum Beispiel nur doc und nicht code Segmente. Aber von der Syntax sind diese nicht sauber voneinander getrennt zudem weiß ich noch nicht wie man Verschachtelungen lösen könnte. Eine Lösung dafür habe ich noch nicht aber ich möchte noch ein paar tolle Features ins Gespräch bringen: Ein eigenen Abarbeitungsstrang für Testing, Debug-Ausgabe oder graphviz-Ausgabe des Calltrees.

Zudem möchte ich noch etwas zu der Visualisierung sagen. Auch mit den Metainformationen wird der Code noch ziemlich aufgebläht. Man hat einfach nur Code und Metainformationen besser separiert. Daher fände ich eine Unterstützung des Editors hier sehr angebracht. Der könnte zum Beispiel die Codeblöcke hervorheben und Metainformationen in einem anderen Level oder wirklich einer anderen Ebene darstellen. Somit würde man nur den eigentlichen Code sehen und per Klick könnte man zum Beispiel die Dokumentation oder Pre und Postconditions bearbeiten.

Es stecken schon viele meiner Gedanken in dem Thema. Allein an dem Text hab ich nun sicher 3 Wochen immer mal wieder rum geschrieben und neu angefangen. Ich würde gerne eine Implementierung mal probieren aber Ruby erscheint mir dafür die falsche Sprache. Aber genug. Ich habe schon wieder viel zu lang, viel zu schwammig und zu breit eine Idee erklärt. Vielleicht hat sich ja tatsächlich jemand die Zeit genommen es zu lesen und vielleicht gibt es sogar in Abschnitten Sinn. Schreibt mich an oder kommentiert wenn ihr Fragen habt oder die Idee euch gefällt.

Comments:

(howto comment?)

Ich hab mir die letzten beiden Tage Zeit genommen, deinen Text zu lesen (incl. der verlinkten Seiten). Das Thema (bzw. einige der Themen) beschäftigen mich auch schon lange und ich habe auch noch keinen perfekten Weg gefunden.

Ich persönlich finde die Kombination aus Code + BDD/Unittests + einer informativen Beschreibung in den meisten Fällen eine ausreichende Kombination. Sauberer Code braucht weniger Kommentare, weil er einfach verständlicher und lesbarer ist. Für Asserts/Pre/Post-Conditions/Tests sind Unit-Tests zwar kein vollständiger Ersatz, doch lässt sich damit einiges abdecken.

Was die Dokumentation angeht: Der meiste Code ist ohnehin in einzelnen Modulen oder Bereichen eingeteilt. Zwischen den Modulen entstehen Schnittstellen. Verwendet irgend jemand “fremdes” meinen Code, interessiert ihn primär die Schnittstelle, nicht die eigentliche Implementierung. Will man einen Fehler fixen, muss man sich sowieso in den Code einarbeiten. Deshalb finde ich es extrem wichtig, diese Schnittstellen sauber zu definieren/planen und auch konsistend und so intuitiv (für den Nutzer) wie möglich umzusetzen. Eine Dokumentation dieser Schnittstellen kann dann von mir aus an jeder Stelle stattfinden. Ob das jetzt in einer LaTeX-Doku festgehalten ist, per JavaDoc angeheftet wurde oder am Anfang der Datei beschriebe nist, ist mir dann eigentlihc egal. Aber das Problem der inkonsistenz bleibt natürlich bestehen :(

Die Ansätze von dir finde ich aber dennoch nicht verkehrt oder schlecht. Ganz im Gegenteil: Solche Erweiterungen (gerade das interaktive Abrufen von Dokumentation) fände ich genial. Ich erwische mich oft dabei, in irb the “methods”-Funktion aufzurufen und dann die Liste zu durchwühlen und anhand vom Namen zu erraten was die Methode eigentlich macht. Am Ende schau ich dann doch wieder auf APIDock nach, weil mir die Information nicht ausreicht.

Zum Thema “Optimierung einstreuen” hatten wir in unserer Thesis ja schon einen echt schicken Ansatz, leider ist das so mit den heutigen Mainstream-Programmiersprachen nicht möglich (Meta-Ebenen).

Deine Vision einer IDE mit mehreren “Layern” finde ich übrigens genial! Sowas würde ich mir beispielsweise auch bei Rails wünschen. Bearbeitet man gerade einen Controller, würde man mit einem Klick zur entsprechenden Stelle in der routes.rb springen können oder in die von dem Controller verwendeten Views bzw. Models. Einen Layer weiter wären dann schon die UnitTests zur Controllermethode.

Viele Grüße, Aaron