Programování pro fyziky 2022/23 – přednáška č. 3

Opakování

To podstatné z první a druhé přednášky bylo toto:

  • zocelujeme se v několika programovacích jazycích (Fortran, Pascal, Python), dnes zmíníme další (Matlab/Octave, C),

  • známe základní vlastnosti dvou numerických datových typů (integer, real),

  • zkusili jsme si klíčové konstrukce strukturovaného programování – cyklus s podmínkou a indexovaný cyklus,

  • porovnávali jsme rychlost provádění cyklů v jednotlivých jazycích.

Dnešní cíl

Probereme příkazy strukturovaného programování.

Přiřazovací příkaz

Je určen pro vyhodnocení výrazu na pravé straně a přiřazení výsledku do cíle na levé straně. Cílem je obvykle jednoduchá (skalární) proměnná, ale může to být i mnohem složitější. Syntaxe:

lvalue:=rvalue;  // Pascal
lvalue=rvalue     ! Fortran
lvalue=rvalue     # Python

Některé jazyky vyžadují statické deklarace datových typů proměnných (Pascal, Fortran, C),

var n : integer; x : real; b : boolean; s : char;       // Pascal
integer n; real(8) x; complex c; logical b; character s  ! Fortran

v jiných proměnné získávají datový typ dynamicky (Python). Vidíme zde deklarace pro standardní numerické datové typy int/integer, real/float a complex, logický typ bool/boolean/logical a znakový typ char/character. S uvedenými deklaracemi (v Pythonu bez deklarací) můžeme proměnné inicializovat např. triviálním přiřazením literálů (doslovně zapsaných hodnot):

n:=1; x:=1/3;            b:=true;  s:='A';  // Pascal
n=1;  x=1._8/3; c=(1,2); b=.true.; s='A'     ! Fortran
n=1;  x=1/3;    c=1+2j;  b=True;   s='A'     # Python

nebo můžeme zachytit výsledky složitějších výrazů:

n:=1+1; x:=arctan(1)*4;                      b:=not b; s:=succ(s);  // Pascal: 2, pi, negace, successor
n=1+1;  x=atan(1._8)*4;   c=sqrt(cmplx(-1)); b=.not.b; s=char(ichar(s)+1)  ! Fortran
n=1+1;  x=math.atan(1)*4; c=cmath.sqrt(-1);  b=not b;  s=chr(ord(s)+1)     # Python

Velmi častým přiřazením je zvětšení hodnoty proměnné o 1:

n:=n+1; x:=x+1.;  // Pascal
n=n+1;  x=x+1.;   !  Fortran, C, Python, Matlab/Octave

Pro tuto akci existují v jazycích různé zkratky: v Pascalu (pro integer, nikoliv pro real) inc(n), v Pascalu, C, Pythonu a Octavu (pro integer i real) lze psát n+=1, x+=1, a dokonce (v C a Octavu) jen n++ nebo ++n, kde první zápis vydá hodnotu a pak ji zvětší a druhý zápis hodnotu nejprve zvětší a pak ji vydá. (Pravověrní céčkaři někdy takto vykládají i název konkurenčního jazyka: C++ nic navíc nepřináší, jen zvyšuje hodnotu jazyka C).

Implicitní konverze vs. dynamický datový typ

V klasických jazycích, kde proměnné mají staticky deklarovaný datový typ, se před přiřazením provádí implicitní konverze datového typu výsledku z pravé strany na typ proměnné na levé straně. Některé konverze jsou ošemetné; např. Pascal raději odmítá automatický převod real na integer, desetinnou část je nutno buď explicitně oříznout funkcí trunc, nebo zaokrouhlit funkcí round:

x:=1; {nelze n:=x;} n:=trunc(x+0.5); n:=round(x+0.5);  // Pascal: přiřazované hodnoty 1.0, 1, 2

V C a Fortranu je přiřazení n=x přípustné a implicitně se při něm ořezává.

Ve skriptovacích jazycích proměnné běžně získávají dynamický datový typ. Jednou z cest je přiřazovací příkaz: v Pythonu sekvence přiřazení a výpisů

a=1; type(a); a=1.; type(a); a=1+0j; type(a)

dokládá, jak s každým přiřazením nastane změna datového typu cíle, nikoliv konverze typu přiřazované hodnoty.

Přiřazení mezi poli

Odbočíme na exkurzi do numerických metod lineární algebry. Tam mj. potřebujeme umět zapisovat vektory (posloupnost čísel indexovatelných jedním indexem, v počítačové terminologii jednorozměrná pole) a matice (indexovatelné dvěma indexy, dvourozměrná pole). Matlab/Octave, Fortran a se snahou i některé další jazyky to umějí pomocí přiřazovacích příkazů, které pracují i s celými poli naráz. Příkazy Matlabu:

A=[1 2;3 4], b=[5;6]

připraví dynamicky matici A a sloupcový vektor b (středníky v literálech na pravých stranách příkazů oddělují jednotlivé řádky),

c=A*b         % c = [17;39]

provede algebraické násobení matice a vektoru, výsledný vektor uloží do proměnné c a tu vypíše. Nyní můžeme vzít matici A a vektor c a řešením soustavy lineárních rovnic \(A\cdot b=c\) se vrátit k vektoru b:

b=A\c         % b = [5;6]

Vidíme zde v akci mocný operátor \ pro řešení soustav lineárních algebraických rovnic v Matlabu. Přiřazovací příkazy zacházející s celými poli jsou pro fyziky vítanou úsporou, ti na polích pracují v jednom kuse (míněno: s poli jako datovými strukturami). Fortranský ekvivalent pro maticové násobení pomocí funkce matmul:

! Fortran a násobení matice s vektorem
integer A(2,2),b(2),c(2)  ! deklarace matice a dvou vektorů
A(1,:)=[1,2]              ! přiřazení prvního řádku matice
A(2,:)=[3,4]
b=[5,6]                   ! přiřazení vektoru
c=matmul(A,b)             ! matmul umí násobit i matici a vektor
print *,c
end program

Když jazyk (třeba Pascal) svou array language nemá, je třeba vše programovat pomocí cyklů procházejících pole po jednotlivých prvcích.

Podmíněné přikazy

Příkaz if

Slouží k podmíněnému provádění příkazů neboli k dvoucestnému větvení podle podmínky, tj. podle výsledku logického výrazu (true/false). Vývojový diagram: conditional (Wikipedia). Syntaxe:

// Pascal a příkaz if
if boolExpr then cmdTrue else cmdFalse;  // plná varianta s oběma větvemi
if boolExpr then cmdTrue;                // zkrácená varianta jen s pozitivní větví
! Fortran a příkaz if
if (logExpr1) then                        ! plná varianta
  cmdsTrue1
else if (logExpr2) then
  cmdsTrue2
...
else
  cmdsFalse
endif
if (logExpr) cmdTrue                      ! zkrácená varianta
# Python a příkaz if
if boolExpr1:                             # plná varianta
  cmdsTrue1
elif boolExpr2:
  cmdsTrue2
...
else:
  cmdsFalse
if boolExpr1: cmdsTrue                    # zkrácená varianta

Pascal připouští ve větvích podmíněného příkazu pouze jediný příkaz a bývá proto nezbytné používat v nich složený příkaz begin ... end. Fortran a Python připouštějí bloky příkazů a složený příkaz tak nepotřebují. V Pythonu se bloky příkazů v jednotlivých větvích vyznačují pomocí povinného odsazování.

Ukázka: sudá čísla. V indexovaném cyklu projdeme celá čísla od 1 do 100 a vypíšeme sudá čísla. Pro test sudosti se hodí zbytek po celočíselném dělení: Pascal pro něj má operátor mod, Fortran funkci mod a Python operátor %. Vypisujeme na jeden dlouhý řádek.

// Pascal
for n:=1 to 100 do
  if n mod 2=0 then write(n,' ');        // výpisy bez odřádkování
writeln;                                 // závěrečné odřádkování
! Fortran
do n=1,100
  if (mod(n,2)==0) print '(x,i0$)',n      ! výpisy bez odřádkování
enddo
print *
# Python
for n in range(1,101):
  if n%2==0: print(n,end=' ')             # výpisy bez odřádkování
print()

Podmíněný výraz

Vítanou úsporou při psaní programů bývá podmíněný výraz, který umožňuje např. sloučit obě větve podmíněného příkazu, zahrnující přiřazení do téže proměnné, do jednoho přiřazovacího příkazu s podmíněným výrazem. V C, kde logický test může zastoupit celočíselná nenula (true) a nula (false), by to vypadalo takto:

#include <stdio.h>
int main() {
  int x,y;
  if (1) { x=2; } else { x=3; }  /* podmíněný příkaz */
  printf("%d\n",x);              /* 2 */
  y=0?2:3;                       /* podmíněný výraz */
  printf("%d\n",y);              /* 3
  return 0;
}

Totéž umí gnuplot, např.: print 1?2:3; print 0?2:3 vypíše nejprve 2, pak 3. Umí to i Fortran, ale pomocí funkce: merge(2,3,.true.); merge(2,3,.false.), a Python: 2 if True else 3; 2 if False else 3.

Příkaz case

Slouží k vícecestnému větvení, často podle hodnot celočíselného výrazu. Pro ukázku vytěžíme hru FizzBuzz, prověřující schopnost hráčů nahrazovat v posloupnosti přirozených čísel násobky 3 slovem Fizz, násobky 5 slovem Buzz a společné násobky obou slovní kombinací FizzBuzz:

1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz ...

FizzBuzz prý bývá oblíbeným námětem pro testování stylu programátorů během přijímacích pohovorů. Implementací se i proto dá na webu najít mnoho, např. na stránce návazného předmětu. Zde volíme rozhodování podle zbytku po dělení společným násobkem 3 a 5:

// Pascal a příkaz case of
var n : integer;
begin
for n:=1 to 100 do
  case n mod (3*5) of
  0:        write('FizzBuzz ');
  3,6,9,12: write('Fizz ');
  5,10:     write('Buzz ');
  else      write(n,' ')
  end;
writeln;
end.
! Fortran a příkaz select case
do n=1,100
  select case (mod(n,3*5))
  case (0);        print '(x,a$)','FizzBuzz'
  case (3,6,9,12); print '(x,a$)','Fizz'
  case (5,10);     print '(x,a$)','Buzz'
  case default;    print '(x,i0$)',n
  end select
enddo
print *
end program
# Python 3.10+ a příkaz match case
for n in range(1,101):
  match n%(3*5):
    case 0:        print('FizzBuzz',end=' ')
    case 3|6|9|12: print('Fizz',end=' ')
    case 5|10:     print('Buzz',end=' ')
    case _:        print(n,end=' ')
print()

Tato varianta vícecestného větvení umožňuje vyhodnotit výraz právě jednou a vejít rovnou do vhodné větve. Totéž pomocí řetězených příkazů if je méně přehledné.

Příkazy cyklu

Cykly s podmínkou while a repeat

Používají se pro opakované provádění (iterování) části programu – těla cyklu, přičemž počet opakování se odvíjí od hodnoty podmínky čili logického výrazu na začátku (příkaz while) nebo konci cyklu (příkaz repeat). Cyklus lze opustit nastavením patřičné hodnoty podmínky nebo předčasně pomocí speciálních příkazů skoku. Vývojové diagramy: while loop (Wikipedia) a varianta cyklu repeat do while loop (Wikipedia). Syntaxe:

// Pascal a cyklus while
while boolExprRun do cmd;         // dokud je podmínka true, prováděj (složený) příkaz
repeat cmds until boolExprBreak;  // opakuj příkazy, než bude podmínka true
continue;                         // skok: pokračuj další iterací
break;                            // skok: opusť cyklus
! Fortran a cyklus do while
do while (logExprRun)              ! dokud je podmínka true, prováděj příkazy
  cmds
enddo
do                                 ! cyklus repeat pomocí nekonečného cyklu s if
  cmds
  if (logExprBreak) exit           ! opakuj příkazy, než bude podmínka true
enddo
cycle                              ! skok: pokračuj další iterací
exit                               ! skok: opusť cyklus
# Python a cyklus while
while boolExprRun:                 # dokud je podmínka true, prováděj příkazy
  cmds
while True:                        # repeat until pomocí nekonečného cyklu s if
  cmds
  if boolExprBreak: break          # opakuj příkazy, než bude podmínka true
continue                           # skok: pokračuj další iterací
break                              # skok: opusť cyklus

Cyklus while obsahuje prováděcí podmínku: při jejím splnění se provede jeden průchod tělem cyklu a následuje návrat k testování podmínky. Cyklus repeat obsahuje ukončovací podmínku: při jejím splnění po průchodu těla cyklu bude cyklus ukončen. Příkazem skoku continue (Fortran: cycle) se předčasně ukončí průchod těla cyklu a přejde se na testování podmínky. Příkazem skoku break (Fortran: exit) se předčasně ukončí celý cyklus a chod programu pokračuje za cyklem. Tělo cyklu while smí v Pascalu obsahovat právě jeden příkaz, tedy obvykle složený příkaz; tělo cyklu repeat smí v Pascalu obsahovat více příkazů.

Cyklus while se vyskytuje ve všech našich programovacích jazycích. Cyklus repeat je vzácnější, ale lze jej srozumitelně simulovat kombinací cyklu while s podmíněným příkazem if a skokem break na konci. Cyklus while zde vytváří nekonečný cyklus, tedy konstrukci, která by se prováděla donekonečna, pokud by nebyla přerušena příkazem skoku nebo vnějším zásahem, např. klávesovou kombinací Ctrl+C (jako Cancel), Ctrl+Break, nebo vypnutím počítače. V elementární podobě jsou nekonečné cykly v Pascalu tyto:

// Pascal: nekonečné cykly pomocí while a repeat
while true do;       // nekonečný cyklus pomocí while
repeat until false;  // nekonečný cyklus pomocí repeat

Připomeneme ukázku z minulé přednášky – součet aritmetické posloupnosti pomocí cyklu while:

// Pascal: sumace pomocí while
var
  n,i : integer;
  s : real;
begin
  readln(n);
  i:=0; s:=0;
  while i<n do begin  // cyklus s prováděcí podmínkou
    i:=i+1; s:=s+i;
  end;
  writeln(n,s);
end.

Pomocí cyklu repeat, tedy s alespoň jedním průchodem a ukončovací podmínkou za tělem cyklu, lze totéž zapsat takto:

// Pascal: sumace pomocí repeat
var
  n,i : integer;
  s : real;
begin
  readln(n);
  i:=0; s:=0;
  repeat              // cyklus s ukončovací podmínkou
    s:=s+i; i:=i+1;
  until i>n;
  writeln(n,s);
end.

Příklad: počítačové epsilon. Víme, že \(1 + 1/2^n > 1\) pro konečné \(n\). Počítači to je jedno, jeho procesor zvládá reálnou aritmetiku jen s omezenou přesností. S jak omezenou, to napoví počítačové epsilon neboli nejmenší kladné číslo, pro které 1+eps>1. (Alternativní definice je o největším kladném čísle, pro které 1+eps==1. To bude poloviční.) Zjistíme ho cyklem s podmínkou, v jehož těle se půlí hodnota v proměnné eps:

! Fortran: machine epsilon... single, double, extended, quadruple
real eps  ! real(4), real(8), real(10), real(16), 1._4, 1._8, 1._10, 1._16
eps=1; do while (1+eps*0.5>1); eps=eps*0.5; enddo; print *,eps,epsilon(1.)
end program

Fortran (gfortran) zpřístupňuje všechny tři úrovně přesnosti reálné aritmetiky (single, double, extended) dostupné na procesorech dnešních osobních počítačů a přidává jednu úroveň softwarově emulovanou (quadruple). Stejný výsledek jako náš výpočet nabízí i fortranská funkce epsilon() s reálným argumentem. Default popis real můžeme nahradit specifickým popisem real(4), real(8), real(10) a real(16), nebo v argumentu funkce psát 1._4, 1._8, 1._10, 1._16, anebo ponechat ukázku v default zápisu a překládat s jednou z voleb gfortran -fdefault-real-8, -fdefault-real-10 a -fdefault-real-16. Dostaneme tak čtyři varianty počítačového epsilon: 1.2e-7, 2.2e-16, 1.1e-19 a 1.9e-34.

Pascal, C, Matlab a Python mají jako default přesnost double a vracejí tedy přibližně 2.2e-16:

// Pascal: machine epsilon
eps:=1; while 1+eps*0.5>1 do eps:=eps*0.5; writeln(eps);
% Matlab/Octave: machine epsilon
format long
eps=1;         while 1+eps*0.5>1, eps=eps*0.5; end; eps  % dvojitá přesnost (double)
eps=single(1); while 1+eps*0.5>1, eps=eps*0.5; end; eps  % jednoduchá přesnost (single)
# Python: machine epsilon
eps=1
while 1+eps*0.5>1: eps=eps*0.5                           # dvojitá přesnost (float)
print(eps)

Indexovaný cyklus for

Používá se pro opakované provádění těla cyklu, přičemž počet iterací se stanoví v hlavičce cyklu pomocí počáteční a konečné meze indexu (řídicí proměnné). Index je obvykle celočíselný, krok často jednotkový, kladný i záporný (ale nenulový). Cyklus lze opustit předčasně pomocí speciálních příkazů skoku. Vývojový diagram: for loop (Wikipedia). Syntaxe:

// Pascal a cyklus for
for i:=first to last do cmd;      // prováděj příkaz pro i od počáteční do koncové meze s krokem 1
for i:=first downto last do cmd;  // totéž s krokem -1 (obvykle zde first >= last)
continue;                         // skok: pokračuj další iterací
break;                            // skok: opusť cyklus
! Fortran a cyklus do
do i=first,last,step               ! prováděj příkazy pro i od počáteční do koncové meze s krokem step
  cmds
enddo
cycle                              ! skok: pokračuj další iterací
exit                               ! skok: opusť cyklus
# Python a cyklus for
for i in range(first,last,step):   # prováděj příkazy pro i od počáteční do (nezahrnuté) koncové meze s krokem step
  cmds
continue                           # skok: pokračuj další iterací
break                              # skok: opusť cyklus

Pro naznačení, co všechno cyklus for vlastně dělá, zapíšeme příkaz for i:=first to last do cmd; pomocí cyklu while. Není to přesný ekvivalent – obě varianty se mohou ve specifických situacích chovat různě.

// Pascal: cyklus for-to pomocí cyklu while
i:=first;               // nastav počáteční mez indexu
while i<=last do begin  // opakuj, dokud je index menší než nebo stejný jako koncová mez
  cmd;                  // proveď příkaz
  inc(i);               // přejdi k následující hodnotě indexu
end;

Různé jazyky a jejich překladače implementují cyklus for v detailech různě. Např. po cyklu s kladným krokem a first>last, jehož tělo se neprovádí, může index udržet svou předchozí hodnotu (Free Pascal) nebo nabýt hodnoty first (varianta s while výše) nebo last. Po řádném doběhnutí cyklu, tedy po last–first+1 iteracích, může mít index hodnotu last (Free Pascal) nebo last+1 (varianta s while výše, také Fortran). Po ukončení skokem index zachovává aktuální hodnotu.

Zopakujme z minulé přednášky součet aritmetické posloupnosti cyklem for:

// Pascal: sumace pomocí indexovaného cyklu
var
  n,i : integer;
  s : real;
begin
  readln(n);
  s:=0;
  for i:=1 to n do begin  // indexovaný cyklus
    s:=s+i;
  end;
  writeln(n,s);
end.

Reálný index v cyklu for

Některé jazyky dovolují užít v cyklu nejen celočíselný index. Pascal připouští index ordinálního datového typu (integer, boolean, char): při var z : char lze vypsat abecedu cyklem for z:='a' to 'z' do write(z). Někdy se mohou hodit i cykly s reálným indexem, jsou však riskantní a v některých jazycích (v Pascalu) zakázané.

! Fortran a indexovaný cyklus s reálným indexem
real(4) x4; real(8) x8
do x4=0,1,.1; print *,x4; enddo   ! 4bytová přesnost: 11 průchodů
do x8=0,1,.1; print *,x8; enddo   ! 8bytová přesnost: 10 průchodů
end program
/* C a neceločíselné krokování */
#include <stdio.h>
int main() {
  float x4; double x8;
  for (x4=0; x4<=1; x4=x4+0.1) { printf("%25.20f\n",x4); } // 4bytová přesnost: 10 průchodů
  for (x8=0; x8<=1; x8=x8+0.1) { printf("%25.20f\n",x8); } // 8bytová přesnost: 11 průchodů
}
% Matlab/Octave a neceločíselné krokování
format long
for x=0:.1:1;         disp(x); end;  % 8bytová přesnost: 11 průchodů
for x=0:single(.1):1; disp(x); end;  % 4bytová přesnost: 11 průchodů

Tyto cykly mají postupovat od 0 do 1 s reálným krokem 0.1. Bohužel proběhnou někdy 10krát, jindy 11krát, a poslední vypsaná hodnota může být větší než koncová mez. Vidíme tu opět v akci reálnou aritmetiku s omezenou přesností: hodnotu 0.1 nelze reprezentovat přesně, platí pro ni: přesná 0.1 < 0.1 v přesnosti double < 0.1 v přesnosti single, a při kumulaci těchto 0.1 do proměnné x probíhá zaokrouhlování v různých přesnostech různě.

Map-Filter-Reduce

Ukázali jsme si imperativní strukturované programování pomocí podmínek a cyklů. Toho se budeme převážně držet i nadále. Naznačíme si nyní dvě alternativy, s nimiž lze podmínky a cykly zapouzdřit do deklarativních konstrukcí zvaných map–filter–reduce. Mapováním můžeme rozumět to, co cyklus for provádí se svým indexem, filtrování chápejme jako akci příkazu if v cyklu for a redukcí nazvěme operaci, která více dat redukuje na jediný údaj (např. sumace, součin nebo extrémy posloupnosti).

Array programming

První oblíbenou možností, našimi jazyky (s výjimkou Pascalu) dobře podporovanou, je array programming. Umožňuje aplikovat přiřazovací příkazy, operace a funkce nejen na skalární data, ale na celá pole (datové struktury tvořené více skalárními prvky téhož typu) nebo jejich části. Programátor pak nemusí pole procházet pomocí cyklů, ale vyjádří vše jednou ranou. Citovaná wiki stránka o array programming jmenuje Fortran jako kanonický příklad array language, začneme tedy s ním. Ukázkou bude výpočet součtu lichých čísel, \(\sum_{n=1}^N(2n-1)\). K výsledku rovnému \(N^2\) se ovšem propracujeme postupným sčítáním.

! Fortran a sumace lichých čísel: imperativní strukturovaný styl
integer,parameter :: nmax=10    ! symbolická konstanta
integer :: a(nmax*2),s          ! statické pole

s=0
do n=1,nmax                     ! cyklus o nmax iteracích počínaje 1
  a(n)=n*2-1                    ! imperativní mapování indexu cyklu na prvek pole
  s=s+a(n)                      ! imperativní redukce (sumace prvků pole)
enddo
print *,nmax,s

s=0
do n=1,nmax*2                   ! cyklus o nmax*2 iteracích
  a(n)=n                        ! imperativní mapování
  if (mod(a(n),2)/=0) s=s+a(n)  ! imperativní filtr a redukce
enddo
print *,nmax,s

end program

V array language se dá stejný problém zapsat takto:

! Fortran a sumace lichých čísel: deklarativní styl pomocí array programming
integer,parameter :: nmax=10    ! symbolická konstanta
integer,allocatable :: aa(:)    ! dynamické (alokovatelné) pole

aa=[(n,n=1,nmax)]               ! iterátor (pole obsahující aritmetickou posloupnost)
aa=aa*2-1                       ! mapování přiřazením mezi poli (přirozená na lichá čísla)
print *,nmax,sum(aa)            ! redukce sumační funkcí

aa=[(n,n=1,nmax*2)]             ! větší iterátor
aa=pack(aa,mod(aa,2)/=0)        ! filtr podle masky (výběr lichých čísel)
print *,nmax,sum(aa)            ! redukce sumační funkcí
! print *,nmax,reduce(aa,fsum)  ! funkcionální redukce, dostupná zatím jen v Intel Fortranu

end program

Cyklus se schoval do iterátoru, zde pole neboli vektoru celočíselných prvků od 1 do nmax. Tuto strukturu jsme v prvním případě přemapovali na posloupnost lichých čísel a tu pak sečetli fortranskou funkcí sum. V druhém případě jsme polem prvků od 1 do nmax*2 pokryli všechna potřebná lichá čísla, ovšem spolu se sudými, a ty jsme dalším příkazem odfiltrovali – funkce mod(aa,2) vytvořila pole zbytků po dělení 2 a relační operace /= (nerovno) z nich vytvořila masku (logické pole) neboli označila jako true nenulové z nich; z pole aa se pak funkcí pack vybraly prvky polohou odpovídající true prvkům masky. Zbylé prvky jsme opět sečetli funkcí sum. Na posledním řádku jsme naznačili i další možnost, jak dosáhnout této redukce (sloučení více prvků do jednoho), a to fortranskou funkcí reduce. Ta je ovšem ve Fortranu natolik nová, že ji zatím implementoval jen Intel Fortran, gfortran nikoliv.

Array programming v Pythonu zpřístupňuje balíček NumPy:

# Python a sumace lichých čísel: cykly vs. NumPy pole
nmax=10

# imperativní strukturovaný styl
a=[0]*nmax                      # alokace seznamu
s=0
for n in range(nmax):           # cyklus o nmax iteracích počínaje 0
  a[n]=n*2+1                    # imperativní mapování
  s+=a[n]                       # imperativní redukce (sumace)
print(nmax,s)

a=[0]*(nmax*2)                  # alokace většího seznamu
s=0
for n in range(nmax*2):         # cyklus o nmax*2 iteracích
  a[n]=n+1                      # imperativní mapování
  if a[n]%2!=0: s+=a[n]         # imperativní filtr a redukce
print(nmax,s)

# deklarativní NumPy styl
import numpy
aa=numpy.arange(nmax)           # iterátor (numpy pole)
aa=aa*2+1                       # mapování přiřazením mezi poli
print(nmax,numpy.sum(aa))       # redukce sumační funkcí

aa=numpy.arange(nmax*2)         # větší iterátor
aa=(lambda n: n+1)(aa)          # funkcionální mapování numpy pole
aa=aa[aa%2!=0]                  # filtr podle masky (pole s bool prvky)
print(nmax,numpy.sum(aa))       # redukce sumační funkcí

NumPy funkce arange(nmax) vytváří podobně jako funkce range aritmetickou posloupnost o nmax prvcích. Datovým typem výsledku je numpy pole, s nímž lze provádět podobné věci, jako jsme viděli výše ve Fortranu. Můžeme kopírovat numpy pole jedním přiřazením, zapisovat výrazy zahrnující numpy pole, nasazovat na numpy pole funkce definované pro skalární argument a konstruovat masky pro výběr (filtrování) prvků z numpy pole.

Funkcionální programování

Druhou možnost, jak zapouzdřit podmínky a cykly do konstrukcí map–filter–reduce, poskytuje funkcionální programování. To shrnuje tyto konstrukce pod společnou střechu funkcí vyššího řádu, jejichž společnou vlastností je, že se odvolávají na first-class funkce definující jejich konkrétní akci (jak přesně mapovat, co filtrovat, na co redukovat). V explicitní podobě je můžeme vidět v Pythonu:

# Python a sumace lichých čísel: funkce map-filter-reduce
nmax=10

# deklarativní funkcionální styl
import functools
aa=range(nmax)                  # iterátor
aa=map(lambda n: n*2+1,aa)      # funkcionální mapování (přirozená na lichá čísla)
print(nmax,sum(aa))             # redukce sumační funkcí

aa=range(nmax*2)                # větší iterátor
aa=map(lambda n: n+1,aa)        # funkcionální mapování (posun o 1)
aa=filter(lambda n: n%2!=0,aa)  # funkcionální filtr (výběr lichých čísel)
print(nmax,functools.reduce(lambda x,y: x+y,aa))  # funkcionální redukce (sumace)

Iterátor vytvořený např. funkcí range mapujeme funkcí vyššího řádu map odvolávající se na (jednovýrazovou anonymní) lambda funkci definovanou pro skalární argument. Výsledný iterátor v prvním případě redukujeme sečtením ve funkci sum, v druhém případě nejprve filtrujeme funkcí vyššího řádu filter pomocí předpisu daného bool lambda funkcí a výsledek redukujeme na součet funkcí vyššího řádu functools.reduce s lambda funkcí vyjadřující sumaci.

Tak jako ve strukturovaném programování zdánlivě zmizely příkazy skoku, tak zde přestaly být patrné podmínky a cykly.

Příkazy skoku a výjimky

Vrátíme-li se ke strukturovanému programování, hlavním otloukánkem je zde obecný příkaz skoku. Jazyky ho poskytují v podobě příkazu goto label, kde návěští label je identifikátor nebo číselný kód, označující řádek v aktuální programové jednotce. Na ten se má nesekvenčně přeskočit, jak jsme viděli dříve v ukázkách spaghetti code. Je radno se skoku goto vyhýbat a ve specifických kontextech jej nahrazovat vhodnějším příkazem. Viděli jsme skoky pro předčasné ukončení cyklu (continue a break, resp. fortranské cycle a exit) a uvidíme skok pro předčasné opuštění procedury (exit, ve Fortranu return).

Další situací vyvolávající potřebu někam si odskočit je vznik chybového stavu programu neboli vyvolání softwarové výjimky (exception). Překladače vkládají do programů kód se standardním ošetřením softwarových výjimek, nejčastěji vedoucí k ukončení programu, některé programovací jazyky však programátorovi dovolují reagovat na výjimky vlastním kódem. V následujících konstrukcích se provádějí příkazy v části try, a nastane-li chyba (zachytí-li se výjimka), přeskočí se na příkazy v části except, resp. catch:

// Free Pascal
try commandsTry except commandsExcept end;  // konstrukce může být složitější
# Python
try:
  cmdsTry
except exception:
  cmdsException ...
else:
  cmdsElse
finally:
  cmdsFinally
% Octave
try
  commandsTry
catch
  commandsCatch
end

Poté program pokračuje sekvenčně příkazem za konstrukcí try.

Příklad: dělení nulou. Dělení reálné nenuly nulou vrátí jako výsledek znaménkové nekonečno (±Infinity, ±Inf) a dělení nuly nulou nečíslo (Not-a-Number, NaN); obojí se někdy označuje za hardwarové výjimky zachycované procesorem (floating-point exceptions), které mohou a nemusí vyvolat softwarovou výjimku. V Octavu hardwarové výjimky nevadí a program pokračuje bez softwarové výjimky, v Pascalu a Pythonu se softwarová výjimka vyvolá:

// Free Pascal
for i:=-1 to 1 do writeln(1/i:4:1);  // výpis -1.0 a chyba Runtime error 200 Division by zero (Lazarus: External SIGFPE)
# Python
for i in (-1,0,1): print(1./i)        # výpis -1.0 a chyba ZeroDivisionError: float division by zero
% Octave
for i=-1:1, 1/i, 0/i, end             % výpis -1 0, Inf NaN, 1 0

Nevyhovuje-li standardní ošetření softwarové výjimky, může programátor připravit vlastní postup pomocí try. V Octave jsou hardwarové výjimky neškodné a větev catch je nečinná, v Pascalu (v Linuxu, nikoliv ve Windows) a Pythonu větev except už v akci uvidíme:

// Free Pascal
for i:=-1 to 1 do
  try writeln(1/i:4:1) except writeln('error') end;  // výpis -1.0, error, 1.0 (Linux) nebo totéž jako bez try (Windows)
# Python
for i in (-1,0,1):                                    # výpis -1.0, error, 1.0
  try: print(1./i)
  except: print('error')
% Octave
for i=-1:1, try, 1/i, 0/i, catch, 'error', end, end   % výpis -1 0, Inf NaN, 1 0

Příkazy pro vstup a výstup dat

Vstupní data lze do programu vpravit pomocí přiřazovacích příkazů ve zdrojovém textu nebo pomocí příkazů pro vstup dat z klávesnice nebo z diskového souboru. Výstupní data mohou pomocí příkazů pro výstup mířit na obrazovku nebo do diskového souboru. Zde se soustředíme na vstup z klávesnice a výstup na obrazovku, ke čtení a zápisu souborů se vrátíme později. Frekventovanými slovy programovacích jazyků pro příkaz vstupu jsou read, scan a input, pro výstup se užívá print, write, disp aj. Někdy (Pascal, C) je vstup a výstup formálně chápán jako příkaz volání příslušné standardní procedury či funkce.

Syntaxe v Pascalu – vstup: read(data), readln(data), výstup: write(data), writeln(data). Varianty s koncovkou ln provedou odřádkování (čteme read-line, write-line).

V C se nabízí dvojice scanf a printf, ve Fortranu read a print/write, v Octave input a disp a v Pythonu input a print. V interaktivním režimu Octavu a Pythonu je běžné vypsat hodnotu výrazu pouhým jeho zápisem.

Vstup a kontrolní výpis dat. Na výzvu odpovíme zadáním čísla z klávesnice a údaj vypíšeme s implicitním i explicitním formátováním. Pascal může explicitně formátovat jednoduchým uvedením délky výpisu za výraz, např. writeln(1:2,3.:4:1) pro 2znakový výpis celočíselné 1 a 4znakový výpis reálné 3. s 1 znakem za desetinnou tečkou (tj. _1_3.0). V jazycích dominuje formátování inspirované funkcí printf jazyka C. Ta obsahuje formátové specifikace, pomocí nichž se formátují výstupy celočíselné (%d, %nd), reálné (%f, %nf, %n.nf, %n.ne), znakové (%s, %ns) ad. Free Pascal poskytuje v knihovně sysutils funkci format, jejímž prvním argumentem je řetězec s obdobnými formátovými specifikacemi. Fortran používá specifikace I pro integer, F a E pro real, L pro logical a A pro znaky.

// Pascal
write('Zadej n: ');   readln(n);      writeln('n = ',n); writeln(format('%s%d',['n = ',n]));
(* C *)
printf("Zadej n: ");  scanf("%d",&n);                    printf("%s%d\n","n = ",n);
// C++
cout << "Zadej n: ";  cin >> n;       cout << "n = " << n;
! Fortran
print *,'Zadej n: ';  read *,n;       print *,'n = ',n;  print '(A,I0)','n = ',n
# Python 2 a 3
n=input('Zadej n: ');                 print('n = ',n);   print('%s%d'%('n = ',n))
n=input('Zadej n: '); n=int(n);       print('n = ',n);   print('%s%d'%('n = ',n))
% Octave
n=input('Zadej n: ');                 n,                 printf('%s%d\n','n = ',n)

Stojí za to si zkusit a pochopit, co provedou formátové specifikace %d, %0d, %3d, %0.3d, %5.3d celočíselné 1, jak to dopadne s reálnou 1. po výpisu specifikacemi %f, %0f, %0.3f, %7.3f a %e, %0e, %0.3e, %10.3e a jak se vypíše řetězec abc specifikacemi %s, %0s, %5s, %0.2s, %5.2s.

Pozastavení programu. Volání readln bez argumentů slouží k pozastavení programu. Pokračuje se stisknutím klávesy Enter:

program prg;
begin
  write('Press Enter to continue . . . ');
  readln;
end.

Přesměrování standardního vstupu a výstupu

Následující aparát dovoluje v operačních systémech (Windows i Linux) nahradit textový vstup z klávesnice čtením dat ze souboru a textový výstup na obrazovku zápisem dat do souboru, aniž by bylo třeba něco dělat se samotným programem. Příkaz operačního systému echo vypíše své argumenty na obrazovku. Přidáme-li k takovému příkazu symbol > a jméno souboru,

echo ahoj > pozdrav.txt          # pokud existoval soubor pozdrav.txt, vytvoří se nanovo a zapíše se do něj výpis příkazu

vytvoří se soubor pozdrav.txt a bude obsahovat text ahoj, který se nevypsal na obrazovku. Zopakujeme-li stejnou sekvenci, soubor pozdrav.dat se přepíše, zapíšeme-li variantu

echo ahoj >> pozdrav.txt         # pokud existoval soubor pozdrav.txt, přidá se k němu výpis příkazu

k souboru se přidá další řádek. Symbolem > jsme přesměrovali standardní výstupní kanál. Existuje i druhý, chybový výstupní kanál, který lze přesměrovat symbolem 2>:

mkdir adresar                    # vytvoří adresář, čímž je znemožněno jeho znovuvytvoření
mkdir adresar                    # chybový výstup neřešen
mkdir adresar > err.txt          # chybový výstup nepotlačen
mkdir adresar 2> err.txt         # chybový výstup přesměrován

Přesměrovat lze oba kanály nezávisle, i oba propojit do společného souboru:

command > std.txt 2> err.txt     # standardní výstup do std.txt, chybový výstup do err.txt
command > out 2>&1               # oba výstupní kanály do souboru out

Volbou speciálních jmen souborů lze výstup zcela potlačit:

command > nul                    # Windows; potlačení standardního výstupu
command > /dev/null              # totéž v Linuxu

Přesměrovat lze i vstup tak, aby se místo z klávesnice načítal ze souboru:

sort                             # setřídí abecedně řádky vložené z klávesnice
sort < in                        # setřídí abecedně řádky načtené ze souboru in
sort < in > out                  # setřídí abecedně řádky načtené ze souboru in a vše zapíše do souboru out

Přesměrovat lze i výstup jednoho programu na vstup druhého programu (této variantě se říká roura):

(echo 2 && echo 1) | sort        # Windows i Linux; program sort setřídí vstup zaslaný dvojicí programů echo
(echo 2; echo 1) | sort          # Linux

což je výsledkem ekvivalentní sekvenci:

echo 2 > tmp && echo 1 >> tmp && sort < tmp && del tmp  # Windows; ukládá se do mezisouboru tmp, který se nakonec smaže
echo 2 > tmp; echo 1 >> tmp; sort < tmp; rm tmp         # Linux

Pokud je žádoucí vidět standardní výstup na obrazovce a souběžně jej kopírovat do souboru, hodí se k tomu linuxovský program tee:

echo ahoj | tee pozdrav.txt      # Linux; zapisuje se souběžně na standardní výstup a do souboru pozdrav.txt)

Cest k tee ve Windows je více (PowerShell, Windows Subsystem for Linux, GnuWin packages), a zkusit lze i tento trik:

echo ahoj > pozdrav.txt | type pozdrav.txt