Forum: Compiler & IDEs Mehrere Instanzen einer Funktion


von Christian (Gast)


Lesenswert?

Hallo,
ich hoffe ich bin hier richtig:

Ich habe eine Filterfunktion in C, welche im Programm für verschiedene 
Signale brauche.
Der Filter, im einfachsten Fall z.B. ein gleitender Mittelwert, benötigt 
für jedes Signal, dass ich Filtern möchte, einen eigenen Puffer.

Wie realisiert man das jetzt am besten in C?

Hier mal ein bisschen PseudoCode der das Problem beschreibt.
1
void main()
2
{
3
 while(1)
4
 {
5
   filt_Drehzahl = filter(get_Drehzahl());
6
   filt_Strom = filter(get_Strom());
7
8
   print(filt_Drehzahl);
9
   print(filt_Strom);
10
11
   //(...usw)
12
 }
13
}
14
15
uint16_t filter(uint16_t neuer_wert)
16
{
17
   int i = 0;
18
   static uint16_t buffer[10];
19
20
   for (i=9;i>=0;i--)
21
   {
22
      buffer[i] = buffer[i-1];
23
   }
24
   buffer[0] = neuer_wert;
25
26
 return sum(buffer)/10;
27
}

Nun brauche ich ja für jedes Signal einen anderen static buffer. Wie 
mache ich das am besten?
Den Buffer um eine Dimension erweitern und eine weitere Variable für den 
Kanal übergeben?
1
static uint16_t buffer[5][10];   // buffer[signal][bufferwerte]
Hat da jemand ein gutes Konzept für?

Gruß
Christian

von Udo S. (urschmitt)


Lesenswert?

Du übergibst den Buffer auch der Filterfunktion.
Dann kannst du mit verschiedenen Buffern verschiedene Filterungen quasi 
parallel machen.

Nachtrag:
Präziser: den Zeiger auf den Puffer und seine Länge als 2 getrennte 
Variablen

: Bearbeitet durch User
von B. S. (bestucki)


Lesenswert?

Der Aufrufer der Funktion, in deinem Fall main, soll den Buffer anlegen. 
Der Funktion wird dann nur einen Zeiger auf den Buffer und dessen Grösse 
übergeben.

Du kannst die ganzen Informationen auch in einen Struktur packen und 
dort gleich einen Index einführen, der auf den ältesten Eintrag zeigt. 
So sparst du dir auch das Umkopieren der einzelnen Werte. Der Funktion 
übergibst du dann einen Zeiger auf die Struktur.

So könnte die Struktur aussehen:
1
typedef struct{
2
  uint16_t * Buffer;
3
  size_t BufferSize;
4
  size_t Oldest;
5
}SignalBuffer;


EDIT:
Die Funktion allgemein zu halten hat den Vorteil, dass du sie auch in 
anderen Projekten wieder verwenden kannst. Ausserdem kannst du weitere 
Filterfunktionen schreiben, ohne etwas Neues erfinden zu müssen, da sie 
mit der selben Struktur arbeiten. Damit kannst du dann verschiedene 
Signale durch verschiedene Filter jagen, ohne irgendetwas konvertieren 
zu müssen.

: Bearbeitet durch User
von Karl H. (kbuchegg)


Lesenswert?

be stucki schrieb:

> Du kannst die ganzen Informationen auch in einen Struktur packen und
> dort gleich einen Index einführen, der auf den ältesten Eintrag zeigt.

Und auch die Summe gleich mit dazu.
Es gibt keinen Grund, warum in der jetzigen Filter Funktion diese Summe 
über alle 10 Werte wieder erneut ausgerechnet werden muss, in dem alle 
Einzelwerte aufsummiert werden müssen.

Die aktualisierte Summe ist einfach die alte Summe weniger dem ätesten 
Wert (der steht im Array und man kriegt ihn mit dem Index eben dieses 
ältesten Wertes) plus dem neuen Wert.
1
typedef struct{
2
  uint16_t * Buffer;
3
  size_t     BufferSize;
4
  size_t     Oldest;
5
  uint16_t   Summ;
6
}SignalBuffer;
7
8
9
10
uint16_t filter(SignalBuffer* buffer, uint16_t neuer_wert)
11
{
12
  buffer->Summ = buffer->Summ - buffer->Buffer[buffer->Oldest] + neuer_wert;
13
  buffer->Buffer[buffer->Oldest] = neuer_wert;
14
  buffer->Oldest++;
15
  if( buffer->Oldest == buffer->BufferSize )
16
    buffer->Oldest = 0;
17
18
  return buffer->Summ / buffer->BufferSize;
19
}
20
21
uint16_t DrehzahlWerte[DREHZAHL_BUFF_SIZE];
22
SignalBuffer DrehzahlBuffer = { DrehzahlWerte, DREHZAHL_BUFF_SIZE, 0, 0 };
23
24
int main()
25
{
26
...
27
  filt_Drehzahl = filter( &DrehzahlBuffer, get_Drehzahl() );
28
...

Ob ein uint16_t für die Summe ausreicht, hängt von den konkret zu 
erwartenden Werten ab.

: Bearbeitet durch User
von Jobst Q. (joquis)


Lesenswert?

Genaugenommen gibt es keine Instanzen einer Funktion. Es gibt Instanzen 
eines Objekts und das sind die Daten, die zum Objekt gehören. Doch es 
gibt  Funktionen, die mit Instanzen arbeiten können, und zwar wie oben 
beschrieben mit Strukturen (struct), von denen die Funktionen einen 
Zeiger bekommen, über den sie auf alle Daten der Struktur zugreifen 
können.

Im Prinzip ist das schon objektorientierte Programmierung. Was der 
Prozessor letztendlich tut, ist kaum anders, als wenn es in C++ mit 
Klassen geschrieben worden wäre.

von Hans (Gast)


Lesenswert?

Jobst Quis schrieb:
> Genaugenommen gibt es keine Instanzen einer Funktion. Es gibt
> Instanzen eines Objekts und das sind die Daten, die zum Objekt gehören.

Ganz genau genommen gibt es Instanzen von Klassen bzw. Strukturen. Die 
Instanzen sind die Objekte. :)

von Christian (Gast)


Lesenswert?

Danke für eure Antworten! Ich werde es mit dem Struct versuchen!

von Christian (Gast)


Lesenswert?

Eine Frage noch:
Kann ich nicht auch das BufferArray im Struct erzeugen lassen?
1
typedef struct{
2
  size_t   BufferSize;
3
  double   dBuffer[BufferSize] = 0;
4
  double  *pBuffer = &dBuffer;
5
  size_t   Oldest = 0;
6
  double   Summ = 0;
7
}SignalBuffer;
8
9
SignalBuffer DrehzahlBuffer = {BUFSIZE}

Somit spare ich mir doch die zusätzliche Initialisierung des 
PufferArrays oder?

Gruß,
Christian

von A. H. (ah8)


Lesenswert?

Christian schrieb:
> Kann ich nicht auch das BufferArray im Struct erzeugen lassen?

Grundsätzlich schon, aber so funktionierts nicht.
1
typedef struct{
2
  size_t   BufferSize;
3
  double   dBuffer[BufferSize] = 0; // gleich zwei Fehler:
4
  // 1. die Feldgröße muss eine Konstanze sein (überlege mal, warum)
5
  // 2. Du versuchst, eine Zahl einem Feld zuzuweisen, das kann nicht gehen (dito)
6
  double  *pBuffer = &dBuffer;
7
  // Lies mal im C-Buch Deines Vertrauens den Abschnitt über die 
8
  // Äquivalenz von Pointern und Arrays nach, dann wirst Du sehen
9
  // dass diese Zeile überflüssig ist.
10
  size_t   Oldest = 0;
11
  double   Summ = 0;
12
}SignalBuffer;

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Christian schrieb:
> Somit spare ich mir doch die zusätzliche Initialisierung des
> PufferArrays oder?

Nein, das geht nicht.  Du kannst in einer Typdefinition keinen
Initialisierer angeben.  Initialisierer können nur da stehen, wo
die tatsächliche Definition einer Variablen dann steht (oder in
der OO-Sprache: wo das Objekt instanziiert wird).

Verschiedene Puffergrößen lassen sich aber nicht in einem Datentyp
so unterbringen, wie du dir das vorstellst.  Dafür bräuchte man so
etwas wie eine Template-Funktionalität, die es in C nicht gibt.  Du
müsstest also entweder verschiedene Datentypen anlegen (*), oder den
Puffer in der Struktur selbst per malloc() allozieren.

(*) Es ist nicht ganz trivial, diese dann zuweisungskompatibel für
die aufgerufene Funktion zu machen.  Seit C99 würde es aber prinzipiell
gehen, indem man das variable Array für den Puffer an das Ende der
Struktur legt und in der Definition keine Größe dafür angibt.  Der
Aufrufer muss sich dann natürlich drum kümmern, dass genügend Speicher
dafür zur Verfügung steht, und der Aufgerufene muss irgendwoher wissen,
wie viel er wirklich zugreifen darf.

von Christian (Gast)


Lesenswert?

A. H. schrieb:
> Christian schrieb:
>> Kann ich nicht auch das BufferArray im Struct erzeugen lassen?
>
> Grundsätzlich schon, aber so funktionierts nicht.
> typedef struct{
>   size_t   BufferSize;
>   double   dBuffer[BufferSize] = 0; // gleich zwei Fehler:
>   // 1. die Feldgröße muss eine Konstanze sein (überlege mal, warum)
>   // 2. Du versuchst, eine Zahl einem Feld zuzuweisen, das kann nicht
> gehen (dito)

1.) Ja okay, die Konstante benötigt er vermutlich vorher um den Speicher 
zu reservieren, richtig?
2.) Ich war der Meinung, dass er bei der initialsierung so den ganzen 
Array "nullt"

>   double  *pBuffer = &dBuffer;
>   // Lies mal im C-Buch Deines Vertrauens den Abschnitt über die
>   // Äquivalenz von Pointern und Arrays nach, dann wirst Du sehen
>   // dass diese Zeile überflüssig ist.
>   size_t   Oldest = 0;
>   double   Summ = 0;
> }SignalBuffer;

Stimmt. Wenn ich den Array "ohne Index" aufrufe dann gibt er ja 
automatisch die Adresse zurück, richtig?

Gruß,
Christian

von Christian (Gast)


Lesenswert?

Okay, dann werde ich die Arrays eben selbst initialisieren ;-)

Danke!

von Karl H. (kbuchegg)


Lesenswert?

Christian schrieb:

> 1.) Ja okay, die Konstante benötigt er vermutlich vorher um den Speicher zu 
reservieren, richtig?

Ganz genau.
Definierst du eine Variable von diesem Typ
1
SignalBuffer myBuffer;
dann muss der Compiler ja auch wissen, wieviel Speicher er dafür 
reservieren muss.
Aber es kommt schlimmer. Definierst du ein Array davon
1
SignalBuffer myBuffers[5];
dann muss es ja auch möglich sein, aus der Startadresse des Arrays und 
dem wissen, das man zb auf das 3.te Element im Array zugreifen will, zu 
errechnen, wo denn dieses 3.te Element im Speicher anfängt. Das geht 
aber nur dann, wenn alle SignalBuffer gleich groß sind. Denn die 
Indexoperation
1
  ... = myBuffers[2];
ist nun mal so definiert, dass sie diese Elementadresse in der Form 
'Startadresse des Arrays PLUS ( Elementnummer MAL Elementgrösse )' 
errechnet. Deswegen beginnen Arrray-Indizes ja auch bei 0 und nicht bei 
1.

Das ist nichts anderes als:
Beginnend an der linken Ecke einer Wand hängen eine Reihe von 
Postkästen. Wie weit ist der n-te Postkasten von der Ecke entfernt?
Das kann man nur dann einfach rechnen, wenn alle Postkästen gleich breit 
sind und diese Breite bekannt ist.


> 2.) Ich war der Meinung, dass er bei der initialsierung so den ganzen
> Array "nullt"

Du definierst hier keine Variable.
Alles was du machst ist, du gibst den Bauplan bekannt, wie eine Variable 
von diesem Typ aussehen wird.

In einer Analogie wäre das eine Vereinbarung, dass alle double Variablen 
mit dem Wert 5.0 zu initialisieren sind. Das geht nicht.

> Stimmt. Wenn ich den Array "ohne Index" aufrufe dann gibt er ja
> automatisch die Adresse zurück, richtig?

Richtig.
Der Namen eines Arrays alleine fungiert automatisch als die Startadresse 
dieses Arrays (mit Ausnahme von der Verwendung in sizeof).
Genau deshalb funktioniert ja auch
1
void foo( int * data )
2
{
3
  data[0] = 5;
4
  *data = 8;
5
6
  data[2] = 7;
7
  *(data+2) = 9;
8
}
9
10
int main()
11
{
12
  int values;
13
  foo( values );
14
}

Dieser Array-Pointer Dualismus ist in C ein wesentlicher und wichtiger 
zentraler Punkt.

: Bearbeitet durch User
von Ingo (Gast)


Lesenswert?

Christian schrieb:
> for (i=9;i>=0;i--)
>    {
>       buffer[i] = buffer[i-1];
>    }
Macht keinen Sinn, wenn du zum Schluss eh aufsummierst. Einfach den 
Index hochzählen und neue Daten einfügen...

von W.S. (Gast)


Lesenswert?

Ingo schrieb:
> Macht keinen Sinn

agree.

Das größte Problem der Programmierer hier in diesem Forum ist wohl, daß 
diese Leute glauben, zu allererst in die Tasten hauen zu müssen. 
Nachdenken über's Problem kommt hinterher, wenn's nicht so geht wie 
gedacht.

Filterfunktionen sollten eigentlich IMMER so klein und kurz gehalten 
werden wie nur möglich. Deshalb sollte man jedem Signalfluß nicht nur 
seine eigenen Puffer gönnen, sondern auch seine eigene Filterfunktion. 
Dann braucht man keine Fallunterscheidung, keine aufwendige Erzeugung 
von Argumenten beim Aufruf und erst recht keine albernen for Schleifen, 
die nur die Zeit unnütz totschlagen.

kleines Beispiel:

const
 bufsize = 10;
var
 index : integer;
 buf   : array[0..bufsize-1] of double;
 mean  : double;
 idx   : integer;

procedure FilterMe (NewValue : double);
begin
  mean:= mean - buf[idx] + NewValue;
  buf[idx]:= NewValue;
  idx:= (idx+1) mod bufsize;
end;

Und wenn man bufsize als Zweierpotenz wählt, kann man den Modulo auch 
noch durch ne Maskierung ersetzen. So eine Mini-Funktion kostet nur 
wenig Speicherplatz und ist schnell und braucht nur ein Argument. Alles 
andere wäre umfänglicher und langsamer.

W.S.

von W.S. (Gast)


Lesenswert?

Nachtrag: index ist überflüssig.

von Walter (Gast)


Lesenswert?

W.S. schrieb:
> Das größte Problem der Programmierer hier in diesem Forum ist wohl, daß
> diese Leute glauben, zu allererst in die Tasten hauen zu müssen.
> Nachdenken über's Problem kommt hinterher, wenn's nicht so geht wie
> gedacht.

ich hab jetzt seit über 30 Jahren kein Pascal mehr programmiert und 
verstehe das wohl nicht:
kannst Du mir erklären wie man mit deinem Beispielcode verschiedene 
Signale gleichzeitig filtern kann?

von Yalu X. (yalu) (Moderator)


Lesenswert?

Walter schrieb:
> kannst Du mir erklären wie man mit deinem Beispielcode verschiedene
> Signale gleichzeitig filtern kann?

Das ist in seinem Programm nicht vorgesehen. Der Grund dafür könnte
folgender sein:

W.S. schrieb:
> Das größte Problem der Programmierer hier in diesem Forum ist wohl, daß
> diese Leute glauben, zu allererst in die Tasten hauen zu müssen.
> Nachdenken über's Problem kommt hinterher, wenn's nicht so geht wie
> gedacht.

Duck und wech ;-)

von W.S. (Gast)


Lesenswert?

Walter schrieb:
> ich hab jetzt seit über 30 Jahren kein Pascal mehr programmiert und
> verstehe das wohl nicht:

Kannst du wenigstens lesen?
Im Allgemeinen zitiere ich mich ja nicht selber, aber hier muß es wohl 
mal sein:
W.S. schrieb:
> Deshalb sollte man jedem Signalfluß nicht nur
> seine eigenen Puffer gönnen, sondern auch seine eigene Filterfunktion.

W.S.

von Klaus F. (kfalser)


Lesenswert?

W.S. schrieb:
> Filterfunktionen sollten eigentlich IMMER so klein und kurz gehalten
> werden wie nur möglich. Deshalb sollte man jedem Signalfluß nicht nur
> seine eigenen Puffer gönnen, sondern auch seine eigene Filterfunktion.
> Dann braucht man keine Fallunterscheidung, keine aufwendige Erzeugung
> von Argumenten beim Aufruf und erst recht keine albernen for Schleifen,
> die nur die Zeit unnütz totschlagen.

Ich habe schon lange keinen DSP und keine Filter mehr programmiert, aber 
dieser Ansatz scheint mir ein bischen zu radikal zu sein.
Ersten wurden Funktionen genau deshalb erfunden, um einen Algorithmus 
nur einmal im Quelltext zu haben, und somit die Wartbarkeit zu erhöhen.
5 verschiedene, aber ähnliche Funktionen auf Fehler zu überprüfen ist 
halt die 5-fache Arbeit.
Und zweitens sollte ein Filter mit einer fixen Abtastrate arbeiten.
Wenn die Rechenzeit ausreicht, um in einem Takt-Intervall alle albernen 
For-Schleifen abzuarbeiten, ist es mir egal, ob der Prozessor 
For-Schleifen oder idle-Schleifen abarbeitet.

: Bearbeitet durch User
von Walter (Gast)


Lesenswert?

W.S. schrieb:
> Kannst du wenigstens lesen?

ja, kann ich, vielen Dank für die Beleidigung

ich habe mir nur so einen bescheuerten Programmierstil angewöhnt dass 
ich ein Unterprogramm nur einmal schreibe, obwohl es an verschiedenen 
Stellen im Programm aufgerufen wird. Deshalb meine Nachfrage ob sich das 
mit deinem Programm auch verwirklichen lässt

von W.S. (Gast)


Lesenswert?

So ein Unterprogramm wird für gewöhnlich überhaupt nicht aus dem 
Programm aufgerufen, sondern ist eigentlich immer eine 
Interrupt-Routine, die dann von der Hardware aktiviert wird, wenn ein 
neues Sample vom betreffenden Stream hereinkommt. Genau deswegen 
sollte sie so spartanisch wie möglich sein. Und da ein anderer Stream 
einen anderen Interrupt verursacht, steht dort nochmal der gleiche 
Algorithmus, allerdings mit wahrscheinlich anderen Parametern.

W.S.

von Walter (Gast)


Lesenswert?

W.S. schrieb:
> procedure FilterMe (NewValue : double);

sorry für meine lange zurück liegenden Pascalkenntnisse, ich hatte das 
für eine normale Funktion gehalten, ich wusste nicht dass man damit eine 
Interruptroutine definiert
in C kann man leider nicht so einfach einen neuen Wert von einem Stream 
holen

von Yalu X. (yalu) (Moderator)


Lesenswert?

Folgendes ist jetzt zwar etwas offtopic, aber die ganze Diskussion hier
kommt doch daher, dass man in C (und auch in Pascal, das das gleiche
Ausführungsmodell hat) versucht, eine Funktion zu schreiben, die pro
Aufruf 1 Eingabewert entgegennimmt und 1 Ausgabewert generiert. Da der
Ausgabewert aber keine Funktion nur des aktuellen Eingabewerts, sondern
von den letzten n Eingabewerten ist, muss irgendwo in irgendeiner Form
die Historie gespeichert werden.

Soll, wie vom TE gewünscht, dieselbe Funktion für mehrere Datenströme
verwendet werden, kann der Puffer für diese Historie nicht lokal in der
Funktion angelegt, sondern muss von extern übergeben werden. Wenn aber
die Stelle, wo der Puffer erzeugt wird, und die Stelle, wo die
Filterfunktion aufgerufen wird, innerhalb des Gesamtprogramms weit
auseinander liegen, muss der Zeiger auf diesen Puffer u.U. über zig
Ecken durchgereicht werden, was ziemlich unschön ist.

Ein Filter hat von Natur aus genau einen Eingang und einen Ausgang. Das
sollte sich idealerweise auch in der Funktionssignatur widerspiegeln,
nämlich dadurch, dass sie genau ein Argument und genau einen
Rückgabewert hat. Wird jetzt zusätzlich noch der Pufferzeiger übergeben,
ist dieses Prinzip verletzt. Das ist nicht nur unschön, sondern macht
auch Schwierigkeiten, wenn mehrere Filter unterschiedlichen Typs
verwendet werden, von denen einige noch weitere Informationen (wie z.B.
Filterkoeffizienten) benötigen. Die einzelnen Filter haben dann
unterschiedliche Signaturen, so dass bspw. kein Array mit
Funktionszeigern auf die einzelnen Filter angelegt werden kann, was aber
praktisch wäre, wenn man zwischen unterschiedlichen Filtern umschalten
können möchte.

Mit Closures (siehe http://de.wikipedia.org/wiki/Closure) könnte dieses
Problem sehr elegant gelöst werden. Ein Closure ist ein Zusammenschluss
aus Funktion und Kontext (ind diesem Fall Puffer, Parameter usw.). Damit
hat das Closure Zugriff auf den Puffer und zusätzliche filterspezifische
Informationen, verhält sich aber nach außen hin wie eine ganz normale
Funktion mit einem Argument und einem Rückgabewert. Leider gibt es in C
(und Pascal) dieses Konstrukt nicht. Auch in älteren C++-Standards
konnte man sich etwas Vergleichbares allenfalls als Objekt
zusammenzimmern, was aber etwas umständlich ist, weil man damit für
jeden Filtertyp eine eigene, von einer gemeinsamen Oberklasse
abgeleitete Unterklasse definieren muss, die die filterspezifischen
Informationen als Membervariablen enthält. Auch primitive "Filter", die
ohne Historie auskommen (wie z.B. ein Verstärker, der den Eingabewert
einfach nur mit einem Faktor multipliziert) und deswegen vom Prinzip her
als gewöhnliche Funktionen realisiert werden könnten, müssen umständlich
in solche Objektstrukturen gepackt werden, damit sie aufrufkomatibel zu
den anderen Filtern werden.

Immerhin gibt es seit C++11 auch echte Closures. Aber auch diese haben
mit dem Problem zu kämpfen, dass sie nicht mit gewöhnlichen Funktionen
kompatibel sind. Man kann also bspw. nicht ein Array anlegen, das sowohl
Zeiger auf Closures als auch gewöhnliche Funktionszeiger enthält.

Noch viel besser als die Verwendung von Closures ist es, ein Filter
nicht als Funktion eines einzelnen Eingabewerts, sonder als Funktion der
gesamten Eingabe, also des Signalstroms zu definieren. Entsprechend ist
auch der Rückgabewert die Gesamtheit der Ausgabewerte, also wieder ein
Signalstrom. Das hat gleich mehrere Vorteile:

- Da die Funktion den Eingabedatenstrom als Ganzes bearbeitet, muss sie
  nur ein einziges Mal aufgerufen werden. Deswegen entfällt die
  Zwischenspeicherung von Informationen zwischen zwei Aufrufen.

- Die Funktion ist eine im mathematischen Sinne echte Funktion, d.h. sie
  liefert bei jedem Aufruf mit dem gleichen Argument (dem Eingabestrom)
  immer den gleichen Funktionswert (den Ausgabestrom). Solche Funktionen
  sind i.Allg. leichter zu programmieren und weniger anfällig gegenüber
  Programmierfehlern.

- Man erspart sich explizite Schleifen, um die Eingabedaten der Reihe
  nach abzuklappern.

- Will man mehrere Filter simultan anwenden, schreibt man die Aufrufe
  einfach hintereinander:
1
  ausgabestrom1 = filter1(eingabestrom1);
2
  ausgabestrom2 = filter2(eingabestrom2);
3
  ausgabestrom3 = filter3(eingabestrom3);
4
  ...
5
  // weitere Verarbeitung der Ausgabeströme

  Es ist also im Quellcode keine "Verzahnung" der Filterfunktionen
  erforderlich, was die Übersichtlichkeit erhöht.

Aber so etwas geht in C (und Pascal) erst recht nicht. Zum einen würde
bei obigem Code in C die Filterung nicht simultan erfolgen, und bei
endlosen Eingabeströmen (die ja den Normalfall darstellen) würde das
Programm nie aus dem Aufruf von filter1 zurückkehren.

In datenflussorientierten Sprachen ist so etwas aber Standard, und auch
in Sprachen mit "lazy evaluation" stellt dies kein Problem dar.

Hier ist ein Beispiel dafür, wie kurz und knackig man so etwas in
Haskell lösen kann. Das Programm liest einen Eingabestrom (eine Liste
von Zahlen) von stdin schickt diesen Strom durch vier unterschiedliche
Filter. Die vier Ausgabeströme werden in eine Tabelle formatiert und auf
stdout ausgegeben. Die mit "--" beginnenden Zeilen sind erläuternde
Kommentare:
1
import Data.List
2
import Text.Printf
3
4
5
-- Universelles FIR-Filter, die Filterkoeffizenten coeffs werden normalisiert, so dass ihre Summe 1 ist:
6
7
firFilter coeffs = map (sum . zipWith (*) normalizedCoeffs) . tails
8
                   where normalizedCoeffs = map (/ sum coeffs) coeffs
9
10
11
-- Spezielle Filtertypen ("Neutralfilter", Mittelwert- und BinomialFilter):
12
13
-- Neutralfilter, schickt die Eingangsdaten unverändert an den Ausgang:
14
15
neutralFilter = firFilter [1]
16
17
18
-- Mittelwertfilter über n Werte, alle n Koeffizienten sind 1:
19
20
avgFilter n = firFilter (replicate n 1)
21
22
23
-- Binomialfilter mit n Koeffizienten, der Koeffizientensatz ist die n-te Zeile des Pascalschen Dreiecks:
24
25
binomialFilter n = firFilter (pascalTriangle !! (n - 1))
26
                   where pascalTriangle = iterate nextRow [1]
27
                         nextRow row    = zipWith (+) ([0] ++ row) (row ++ [0])
28
29
30
-- Mehrere Filter (filters) auf den Eingabedatenstrom xs anwenden:
31
32
applyFilters filters xs = map ($ xs) filters
33
34
35
-- Eingabezeichenstrom in Strom von Zahlenwerten parsen:
36
37
parseInput = map read . words
38
39
40
-- Liste der Ausgabedatenströme in eine Tabelle mit je einer Spalte für jedes Filter formatieren:
41
42
formatOutput :: [[Double]] -> String
43
formatOutput = unlines . map formatLine . transpose
44
               where formatLine = concatMap (printf "%10.3f ")
45
46
47
-- Beispielssatz von 4 Filtern (Neutralfilter, Mittelwertfilter über 5 und 10 Werte und
48
-- Binomialfilter mit 10 Koeffizienten):
49
50
allFilters = [ neutralFilter    ,
51
               avgFilter       5,
52
               avgFilter      10,
53
               binomialFilter 10 ]
54
55
56
-- Hauptprogramm, Parsen der Eingabe, Anwendung der vier Filter und Formatierung der Ausgabe
57
-- miteinander verketten:
58
59
main = interact $ formatOutput . applyFilters allFilters . parseInput

Folgende Eingabe
1
     0.293
2
    -0.138
3
     0.488
4
     0.079
5
     0.457
6
     0.321
7
     0.600
8
     0.573
9
     0.748
10
     1.037
11
    ...

führt damit zu folgender Ausgabe:
1
     0.293      0.236      0.446      0.391
2
    -0.138      0.241      0.519      0.482
3
     0.488      0.389      0.626      0.591
4
     0.079      0.406      0.668      0.715
5
     0.457      0.540      0.751      0.833
6
     0.321      0.656      0.778      0.916
7
     0.600      0.797      0.825      0.943
8
     0.573      0.862      0.892      0.925
9
     0.748      0.931      0.953      0.892
10
     1.037      0.962      0.978      0.879
11
    ...

Der Eingabestrom darf dabei endlos sein. Man könnte ihn bspw. mit einem
zweiten Programm in einer Endlosschleife generieren und in das obige
Programm hineinpipen. Genauso könnten die Eingabedaten von einem oder
mehreren ADCs gelesen und die Ausgabe an DACs geschickt werden.

Bemerkenswert ist auch, mit wie wenig Code die einzelnen Filter
realisiert werden. Das geht u.a. deswegen so leicht von der Hand, weil
die Funktion firFilter nicht nur einen Einzelwert, sondern den
kompletten Eingabestrom als Argument empfängt. Eine explizite Pufferung
der Historie ist deswegen nicht erforderlich. Es ist stattdessen die
Aufgabe des Compilers und des Laufzeitsystems, für jedes einzelne Filter
die passende Anzahl von Eingabewerten zu puffern. Der Programmierer muss
sich nur um die Mathematik der Filter kümmern.

Mir ist natürlich klar, dass so etwas nicht auf einem kleinen AVR
implementiert werden kann. Da wird man solche Dinge wie die Pufferung
und Verarbeitungsablauf für die vier Filter noch Stück für Stück im
Detail ausprogrammieren.

Ich wollte mit dem Beispiel vielmehr zeigen, dass wir uns in Zukunft um
viele Dinge, die uns heute als (zumindest kleine) Probleme erscheinen,
nicht mehr kümmern müssen, da sie dank solcher vereinfachenden
Programmierparadigmen an den Compiler delegiert werden können.

Immerhin würde das obige Beispiel schon heute auf einem etwas besser
ausgestatten ARM laufen, wobei allerdings bei höheren Abtastraten die
mangelnde Echtzeitfähigkeit von Haskell eine Grenze setzen wird. Aber
vielleicht kommt ja irgendwann eine Sprache, die wenigstens ein paar
wichtige Vorzüge von Haskell mit der guten Ressourcenkontrolle von C
verbindet.

von Christian (Gast)


Lesenswert?

Danke für deinen ausfürhlichen Beitrag! Ich habe erst jetzt gesehen, 
dass hier noch diskutiert wird ;-)

von Dr. Sommer (Gast)


Lesenswert?

Yalu X. schrieb:
> Man kann also bspw. nicht ein Array anlegen, das sowohl
> Zeiger auf Closures als auch gewöhnliche Funktionszeiger enthält.

Doch, indem man ein Array aus std::function nimmt. Da kann man 
Funktionspointer, Lambdas (Closures), Klassen die operator () 
implementieren, und auch Member-Funktionen+dazugehöriger Instanz 
speichern. Der Nachteil ist, dass dynamischer Speicher ("new") benötigt 
wird - aber denn braucht Haskell auch :P

von Yalu X. (yalu) (Moderator)


Lesenswert?

Dr. Sommer schrieb:
> Yalu X. schrieb:
>> Man kann also bspw. nicht ein Array anlegen, das sowohl
>> Zeiger auf Closures als auch gewöhnliche Funktionszeiger enthält.
>
> Doch, indem man ein Array aus std::function nimmt. Da kann man
> Funktionspointer, Lambdas (Closures), Klassen die operator ()
> implementieren, und auch Member-Funktionen+dazugehöriger Instanz
> speichern.

Hmm, dann habe ich das irgendwie falsch in Erinnerung. Ich hatte vor 
einiger Zeit tatsächlich ein Problem im Zusammenhang mit std::function 
und gewöhnlichen Funktionen. Ich müsste noch einmal danach graben, um zu 
sehen, wo das Problem genau lag.

Vielleicht war es gerade anders herum, d.h. ich wollte an eine 
Subscribe-Methode, die einen Funktionszeiger erwartet, ein Lambda 
übergeben. Kann es sein, dass das nicht geht?

von Dr. Sommer (Gast)


Lesenswert?

Yalu X. schrieb:
Siehe hier, es geht: http://ideone.com/2QAxPf
> Vielleicht war es gerade anders herum, d.h. ich wollte an eine
> Subscribe-Methode, die einen Funktionszeiger erwartet, ein Lambda
> übergeben. Kann es sein, dass das nicht geht?
Ja, das geht nicht. Ein Funktionspointer ist zB 8 Bytes groß auf einer 
Plattform, aber ein Closure ist vielleicht 100 Bytes groß, das kann man 
so nicht übergeben. Man kann nur Lambdas die kein Capture haben 
übergeben. Man muss in der Subscriber-Methode halt ein std::function 
verwenden, oder einen Wrapper mithilfe der üblichen user_data-Pointer 
schreiben, der dann ein std::function "aufruft".

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.