Programování pro fyziky 2023/24 – 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 (Python, Fortran, C, Matlab/Octave),

  • známe základní vlastnosti dvou numerických datových typů (int a float neboli 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í. Uk

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:

# Přiřazovací příkaz
lvalue=rvalue       # Python
lvalue=rvalue       ! Fortran
lvalue=rvalue;     // C

V některých jazycích proměnné získávají datový typ dynamicky (Python), v jiných se vyžadují statické deklarace datových typů (Fortran, C):

# Statické deklarace ve Fortranu a C
integer n; real(8) x; complex c; logical b; character s  ! Fortran
int n; double x; double complex c; bool b               // C

Vidíme zde deklarace pro standardní numerické datové typy int/integer, float/real a complex, logický typ bool/boolean/logical a znakový typ str/string/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):

# Přiřazovací příkazy s literály
n=1; x=1/3;    c=1+2j;  b=True;   s='A'     # Python: 1 0.3333333333333333 (1+2j) True A
n=1; x=1._8/3; c=(1,2); b=.true.; s='A'     ! Fortran

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

# Přiřazovací příkazy s výrazy (2, pi, odmocnina z -1, negace, následník)
n=1+1; x=math.atan(1)*4; c=cmath.sqrt(-1);  b=not b;  s=chr(ord(s)+1)     # Python: 2 3.141592653589793 1j False B
n=1+1; x=atan(1._8)*4;   c=sqrt(cmplx(-1)); b=.not.b; s=char(ichar(s)+1)  ! Fortran

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

n=n+1; x=x+1.; c=c+1.           # Python, Fortran, C, Matlab/Octave

Pro tuto akci existují v jazycích různé zkratky: v Pythonu, C a Octavu (pro integer i real) lze psát n+=1, x+=1,

n+=1; x+=1; c+=1; b+=1; s+='1'  # Python: 3 4.141592653589793 (1+1j) 1 B1

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).

Dynamický datový typ vs. implicitní konverze

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

# Python: dynamický datový typ
a=1; type(a); a=1.; type(a); a=1+0j; type(a)  # int, float, complex

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

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ř. při konverzi z real na integer se oříznutím nebo zaokrouhlením ztrácí desetinná část:

! Fortran: implicitní konverze datového typu
x=1; n=nint(x+0.5); n=int(x+0.5); n=x+0.5
n=int(x+0.5);  print *,n  ! 1 (oříznutí)
n=nint(x+0.5); print *,n  ! 2 (nearest int/zaokrouhlení)
n=x+0.5;       print *,n  ! 1 (implicitní int)
end program

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, Python, 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/Octavu, zdaleka nejstručnější:

% Matlab/Octave: inicializace matice a vektoru
A=[1 2;3 4], b=[5;6]  % včetně implicitního výpisu

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),

% Matlab/Octave: násobení matice vektorem
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:

% Matlab/Octave: řešení soustavy algebraických rovnic
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.

Python pro tutéž akci potřebuje balíček NumPy s řešičem rovnic lineární algebry np.linalg.solve:

# Python a NumPy: maticové násobení A.b a řešení soustavy lineárních algebraických rovnic A.b=c
import numpy as np
A=np.array([[1,2],[3,4]]) # matice 2x2
b=np.array([5,6])         # řádkový vektor; pro sloupcový vektor: b=np.array([[5],[6]])
print(A)                  # A = [[1 2] [3 4]]
print(b)                  # b = [5 6]
c=np.matmul(A,b)
print('c =',c)            # c = [17 39]
b=np.linalg.solve(A,c)
print('b =',b)            # b = [5. 6.]

Fortranský ekvivalent pro maticové násobení pomocí funkce matmul (pro řešení algebraických soustav bychom museli do knihoven, jako Python):

! Fortran: 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í do řádků matice
A(2,:)=[3,4]
b=[5,6]                   ! přiřazení vektoru
c=matmul(A,b)             ! matmul násobí i matici s vektorem
print *,'c =',c           ! c = 17 39
end program

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). Když jazyk 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říkazy

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 v programovacích jazycích obvykle připouští více větví, v podstatě formou řetězení podmíněných příkazů:

# Python: příkaz if
if boolExpr1:                             # plná varianta
  cmdsTrue1
elif boolExpr2:
  cmdsTrue2
...
else:
  cmdsFalse
if boolExpr1: cmdsTrue                    # zkrácená varianta
! Fortran: příkaz if
if (logExpr1) then                        ! plná varianta
  cmdsTrue1
else if (logExpr2) then
  cmdsTrue2
...
else
  cmdsFalse
endif
if (logExpr) cmdTrue                      ! zkrácená varianta

Ve větvích podmíněného příkazu se připouštějí bloky o více příkazech. V Pythonu se bloky jednotlivých větví vyznačují pomocí povinného odsazení, ve Fortranu je třeba podmíněný příkaz uzavřít pomocí enf if.

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í: Python pro něj má operátor %, Fortran funkci mod. Vypisujeme na jeden dlouhý řádek.

# Python: výpis sudých čísel
for n in range(1,101):
  if n%2==0: print(n,end=' ')             # výpisy bez odřádkování
print()                                   # závěrečné odřádkování
! Fortran: výpis sudých čísel
do n=1,100
  if (mod(n,2)==0) print '(x,i0$)',n      ! výpisy bez odřádkování
enddo
print *
end program

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:

// C: podmíněný výraz
#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 Python a Fortran, ten pomocí funkce:

y=2 if True else 3;  print(2 if False else 3)    # Python: podmíněný výraz v přiřazení a výpisu
y=merge(2,3,.true.); print *,merge(2,3,.false.)  # Fortran: funkce pro podmíněný výraz

Příkaz case

Slouží k vícecestnému větvení, často podle hodnot celočíselného výrazu. Umožňuje vyhodnotit testovaný výraz právě jednou a vejít rovnou do příslušné větve. Totéž pomocí řetězených příkazů if je méně přehledné.

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:

# Python 3.10+: 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()
! Fortran: 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

Příkazy cyklu

Cyklus s podmínkou while

Používá 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 logického výrazu čili podmínky stojící obvykle na začátku cyklu (příkaz while), ale i jinde, třeba na konci cyklu. 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 do while loop (Wikipedia). Syntaxe:

# Python: cyklus while
while boolExprRun:                 # cyklus while: dokud je podmínka true, prováděj příkazy
  cmds
while True:                        # cyklus s podmínkou na konci pomocí nekonečného cyklu s if
  cmds
  if boolExprBreak: break          # opakuj příkazy, než bude podmínka true
continue                           # speciální příkaz skoku: pokračuj další iterací
break                              # speciální příkaz skoku: opusť cyklus
! Fortran: cyklus do while
do while (logExprRun)              ! cyklus do while: dokud je podmínka true, prováděj příkazy
  cmds
enddo
do                                 ! cyklus s podmínkou na konci pomocí nekonečného cyklu s if
  cmds
  if (logExprExit) exit            ! opakuj příkazy, než bude podmínka true
enddo
cycle                              ! speciální příkaz skoku: pokračuj další iterací
exit                               ! speciální příkaz skoku: 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. Zde předvedený cyklus s podmínkou na konci obsahuje ukončovací podmínku: při jejím splnění po průchodu těla cyklu bude cyklus opuštěn. Příkazem skoku continue (Fortran: cycle) se předčasně ukončí provádění aktuální iterace 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. Cyklus while se vyskytuje ve všech našich programovacích jazycích, explicitní konstrukce pro cyklus s podmínkou na konci je vzácnější (Octave: repeat).

Cyklus while True 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ě lze nekonečný cyklus v Pythonu a Fortranu psát takto:

# Nekonečný cyklus
while True: pass  # Python
do; enddo         ! Fortran

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

# Python: cyklus s podmínkou pro součet aritmetické posloupnosti
n=100_000_001
i=0; s=0.
while i<n:                     # cyklus s podmínkou
  i+=1; s+=i                   # blok cyklu: update proměnných i a s
print('n,s =',n,s)

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, což souvisí s uložením mantisy v dvojkové soustavě. Python, C a Matlab/Octave mají jako default přesnost double a vracejí tedy přibližně 2.2e-16:

# Python: machine epsilon
eps=1
while 1+eps*0.5>1: eps=eps*0.5                           # dvojitá přesnost (float)
print('eps =',eps)                                       # eps = 2.220446049250313e-16
% 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 = 2.220446049250313e-16
eps=single(1); while 1+eps*0.5>1, eps=eps*0.5; end; eps  % jednoduchá přesnost (single), eps = 1.1920929e-07

Fortran (gfortran) zpřístupňuje všechny tři úrovně přesnosti reálné aritmetiky (single, double, extended) dostupné na procesorech dnešních písíček 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 epsilon 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.

! Fortran: machine epsilon v presnostech real(4), real(8), real(10), real(16)
integer,parameter :: RP=8         ! konstanta pro real precision
real(RP) eps
eps=1; do while (1+eps*0.5>1); eps=eps*0.5; enddo
print *,'eps =',eps,epsilon(eps)  ! vypočtené eps a interní funkce pro totez
end program

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ý (+1), 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:

# Python: cyklus for
for i in range(start,stop,step):   # prováděj příkazy pro i od počáteční do (nezahrnuté) koncové meze s krokem step
  cmds
continue                           # speciální příkaz skoku: pokračuj další iterací
break                              # speciální příkaz skoku: opusť cyklus
! Fortran: cyklus do
do i=start,stop,step               ! prováděj příkazy pro i od počáteční do koncové meze s krokem step
  cmds
enddo
cycle                              ! speciální příkaz skoku: pokračuj další iterací
exit                               ! speciální příkaz skoku: opusť cyklus

Pro naznačení, co všechno cyklus for vlastně dělá, zapíšeme příkaz for i in range(start,stop,step) pomocí cyklu while. Není to přesný ekvivalent – ve specifických situacích se může chovat jinak.

# Python: cyklus for pomocí cyklu while (pro kladný krok)
start=1; stop=11; step=1
for i in range(start,stop,step): print(i,end=' ')
print()
i=start             # nastav počáteční mez indexu
while i<stop:       # opakuj, dokud je index menší než nebo stejný jako koncová mez
# while i>stop:     # varianta pro záporný krok
  print(i,end=' ')  # proveď příkaz
  i+=step           # přejdi k následující hodnotě indexu
print()

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

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

# Python: indexovaný cyklus pro součet aritmetické posloupnosti
n=100_000_001
s=0.
for i in range(1,n+1):  # indexovaný cyklus od 1 do n (bez koncove hodnoty)
  s+=i
print('n,s =',n,s)

Reálný index v cyklu for

Některé jazyky dovolují užít v cyklu nejen celočíselný, ale i reálný index. Pythonský range snese jen int argumenty, funkce np.arange umí i reálné argumenty. Je to však riskantní, v některých jazycích dokonce zakázané.

# Python a NumPy: indexovaný cyklus s reálným indexem při kroku 0.1
import numpy as np
stop=1; mystep=0.1
step=np.float32(mystep)     # 4bytová přesnost
for x in np.arange(0,stop,step):
  print('%4.1f'%x,end=' ')  # 0.0  0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9
print()
step=np.float64(mystep)     # 8bytová přesnost
for x in np.arange(0,stop,step):
  print('%4.1f'%x,end=' ')  # 0.0  0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9
print()
! Fortran: 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: indexovaný cyklus s reálným indexem */
#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: indexovaný cyklus s reálným indexem
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ě.

NumPy a jeho np.arange nezahrnuje horní mez výstupní posloupnosti a zdá se být proti těmto efektům imunní, ale není. Stačí v našem skriptu změnit krok z 0.1 na 0.7 a horní mez z 1 na 7 a máme efekt, který nechceme mít.

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ř. součet, součin nebo extrémy posloupnosti).

Array programming

První oblíbenou možností, našimi jazyky 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    ! deklarace symbolické konstanty
integer :: a(nmax*2),s          ! deklarace statického pole a skaláru

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 =',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 =',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    ! deklarace symbolické konstanty
integer,allocatable :: aa(:)    ! deklarace dynamického (alokovatelného) 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,s =',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,s =',nmax,sum(aa) ! redukce sumační funkcí
! print *,'nmax,s =',nmax,reduce(aa,fsum)  ! funkcionální redukce, dostupná zatím jen v Intel Fortranu
! contains
! pure integer function fsum(x,y); integer,intent(in) :: x,y; fsum=x+y; end function

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ích řádcích 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. NumPy funkce arange(nmax) vytváří aritmetickou posloupnost o nmax prvcích. Datovým typem výsledku je NumPy n-dimenzionální pole (numpy.ndarray), 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.

# Python a sumace lichých čísel: cykly a array language v NumPy
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 =',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 =',nmax,s)

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

aa=np.arange(nmax*2)            # větší iterátor
aa=aa+1                         # mapování přiřazením
aa=aa[aa%2!=0]                  # filtr podle masky (pole s bool prvky)
print('nmax,s =',nmax,np.sum(aa)) # redukce sumační funkcí

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 do 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 Pythonu je můžeme použít v explicitní podobě:

# 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,s =',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,s =',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íněné příkazy 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í funkce (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:

# Python: konstrukce try
try:
  cmdsTry
except exception:
  cmdsException ...
else:
  cmdsElse
finally:
  cmdsFinally
% Matlab/Octave: konstrukce try
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 Pythonu se softwarová výjimka vyvolá, v Matlabu hardwarové výjimky nevadí a skript pokračuje bez softwarové výjimky:

# Python: odmítavá implicitní reakce na dělení 0/0
for i in (-1,0,1): print(1/i,0/i)  # výpis -1.0 -0.0 a chyba ZeroDivisionError: division by zero
% Matlab/Octave: vstřícná implicitní reakce na dělení 0/0
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 Matlabu jsou hardwarové výjimky neškodné a větev catch je nečinná, v Pythonu větev except už v akci uvidíme:

# Python: větev except v akci
for i in (-1,0,1):                                   # výpis -1.0 -0.0, error, 1.0 0.0
  try: print(1/i,0/i)
  except: print('error')
% Matlab/Octave: vstřícná implicitní reakce, větev catch nečinná
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 input, read a scan, pro výstup se užívá print, write, disp aj. Někdy je vstup a výstup formálně chápán jako příkaz volání příslušné standardní funkce.

Syntaxe v Pythonu – vstup: string=input(prompt), výstup: print(objects,sep,end,...). Ve Fortranu se nabízí dvojice read a print/write, v C scanf a printf a v Matlabu input a disp. V Matlabu a interaktivním (REPL) režimu Pythonu je běžné vypisovat hodnotu výrazu pouhým jeho zápisem, tedy x místo (v Pythonu) print(x).

Příklad: vstup a 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. Pro explicitní formátování výpisů slouží jazykům inspirace 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. Toto printf-style string formatting přijal i Python a Octave. Python doplnil ještě formatted string literals s jinak poskládanými specifikacemi. Fortran používá specifikace i pro integer, f a e pro real, L pro logical a a pro znaky.

# Python: vstup z klávesnice (stdin), implicitní a explicitně formátovaný výstup na obrazovku (stdout)
n=input('Zadej n: ');     n=int(n);       print('n =',n);    print('%s %d'%('n =',n))   # printf-style string formatting
                                                             print(f'{"n =":s} {n:d}')  # formatted string literals
// C
printf("Zadej n: ");      scanf("%d",&n);                    printf("%s %d\n","n =",n);
// C++
cout << "Zadej n: ";      cin >> n;       cout << "n = " << n;
% Octave
n=input('Zadej n: ');                     n,                 printf('%s %d\n','n =',n)
! Fortran
print '(a$)','Zadej n: '; read *,n;       print *,'n =',n;   print '(a,x,i0)','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.

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

Ukázkové programy z tohoto textu jsou sbaleny zde: (č. 3) přiřazení, if, cykly, (č. 4) map-filter-reduce, try, print.