Această serie de articole se vrea o comparație între diverse limbaje de programare. În fiecare număr al revistei voi incerca să acopar un spectru cât mai larg și să arăt mici probleme rezolvate în limbaje diferite. Problemele alese vor fi relativ simple, scopul fiind sa ne concentrăm asupra posibilitaților diferitelor limbaje comparându-le intre ele. Chiar dacă sintaxa unui limbaj sau altul vă este nefamiliară, ideea este să intelegeți modul de abordare al problemei, specific fiecărui limbaj în parte. Astfel le putem pune în balanța plusurile și minusurile.
În numărul acesta vom rezolva problema folosind Java, Python 3 și Haskell. Alegerea limbajelor a fost facută astfel încât să fie cat mai diferite. Primul dintre ele este Java, un limbaj imperativ orientat-obiect, static typed. Este poate cel mai folosit limbaj în momentul de față și, fără îndoială, un limbaj de referință.
Al doilea limbaj pe care îl vom folosi astăzi este Python 3, care cu toate că este tot un limbaj imperativ, prin faptul că e dynamic-typed și prin tehnicile de programare funcțională pe care ni le pune la dispoziție, combinate cu cele orientate-obiect ne dă o perspectiva diferită.
Al treilea pe listă, fără sa fie cel din urmă, este Haskell. Acesta e totul și cu totul diferit de celelalte două. În primul rând este un limbaj pur funcțional, efectele secundare produse de funcții fiind specificate în tipul acestora. Apoi este un limbaj static-typed dar cu type-inference. Aceasta înseamnă ca în majoritatea cazurilor declarațiile de tipuri pot lipsi.
Să se scrie un interpretor de comenzi minimal care să citească de la standard input comenzi și să afișeze rezultatul lor. Comenzile acceptate sunt ls și cat
ls
listează conținutul directorului
afișează un mesaj de eroare dacă primește mai mult de un parametru
cat
dacă nu primește nici un parametru afișează mesaj de eroare
Programul se va termina când utilizatorul apasă Ctrl+C sau când va închide fișierul de intrare (Ctrl+Z pe windows respectiv Ctrl+D pe Linux) 17
Urmează implementarea problemei folosind Java. Codul are cca 3000 de caractere pe 110 linii. A fost rulat pe Ubuntu Linux 11.10, folosind openjdk 1.6.0 cu comanda
$ javac Shell.java && java Shell
Fiind un limbaj foarte cunoscut, comentariile referitoare la sintaxă nu cred ca sunt necesare. Urmează codul sursa din care lipsește doar secțiunea import.
public class Shell {
public static void main(final String[] args) {
try {
final BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String s;
System.out.print(">");
while ((s = in.readLine()) != null && s.length() != 0) {
try {
final String output = execute(s);
System.out.println(output);
} catch(Exception ex) {
System.out.println(ex.getMessage());
}
System.out.print(">");
}
System.out.println();
} catch (IOException ex) {
System.err.println(ex);
}
}
private static Map CMD_MAP;
static {
final Map tmpMap = new HashMap();
tmpMap.put("ls", new Ls());
tmpMap.put("cat", new Cat());
CMD_MAP = Collections.unmodifiableMap(tmpMap);
}
private static String execute(final String cmd) {
final String[] splitCmd = cmd.split(" ");
final String cmdName = splitCmd[0];
if (!CMD_MAP.containsKey(cmdName) ) {
throw new RuntimeException("Bad command: " + cmdName);
} else {
final Command commandObject = CMD_MAP.get(cmdName);
final String[] params = Arrays.copyOfRange(splitCmd, 1, splitCmd.length);
return commandObject.execute(params);
}
}
}
Clasa următoare este necesară deoarece biblioteca standard nu are o funcție care să facă join la o lista de String. Funcții similare există în biblioteci third-party dar m-am limitat doar la biblioteca standard.18
class Util {
public static String join(final Object[] items, final String sep) {
final StringBuilder sb = new StringBuilder();
int index = 0;
for (final Object item: items) {
sb.append(item.toString());
if (++index < items.length) {
sb.append("\n");
}
}
return sb.toString();
}
}
Mai jos urmează interfața Command și implementarea ei pentru ls respectiv cat.
interface Command {
public String execute(String[] params);
}
class Ls implements Command {
@Override
public String execute(final String[] params) {
if (params.length > 1) {
throw new RuntimeException("Too many parameters");
} else if (params.length == 0) {
return executeImpl(".");
} else {
return executeImpl(params[0]);
}
}
private String executeImpl(String dir) {
final String[] files = new File(dir).list();
return Util.join(files, "\n");
}
}
class Cat implements Command {
@Override
public String execute(final String[] params) {
final List list = new LinkedList();
if (params.length == 0) {
throw new RuntimeException("No files to cat.");
}
for (final String file: params) {
list.add(catOneFile(file));
}
return Util.join(list.toArray(), "\n");
}
private String catOneFile(final String file) {
try {
final String contents = new Scanner( new File(file) ).useDelimiter("\\Z").next();
return file + ":\n" + contents;
} catch (Exception ex) {
return "Could not open file '" + file + "'";
}
}
}
Implementarea în Python a fost rulată cu CPython 3.2.2 și are cca 1250 de caractere pe 65 de linii. Am ales versiunea 3 a limbajului doar pentru a promova ultima versiune. Codul este în mare parte compatibil cu versiunea 2.7, singura diferență importantă fiind felul în care se face citirea din stdin în bucla "for line în stdin".
Urmează codul cu comentarii în zonele în care am considerat ca sunt expresii sintactice mai puțin clare.
În primul rând, pentru că e un script executabil fișierul incepe cu #! ca orice script linux executabil. Astfel scriptul îl putem rula din linie de comandă în felul următor:
$ ./shell.py
...iar interpretorul de comenzi va știi că la rularea scriputul trebuie să folosească python3 instalat pe mașina respectiva.
#!/usr/bin/env python3
PROMPT = ">"
Mai jos e funcția main care iterează peste liniile din stdin. Observați că funcția prompt este inner în cadrul main, deoarece este singurul loc în care e folosită. Pentru a evita coliziunile de nume, e bine să limităm scopul funcțiilor.
def main():
from sys import stdout,stdin
def prompt():
print (PROMPT, end="")
stdout.flush()
print("Ctrl+C to exit")
prompt()
for cmd in stdin:
try:
print(execute(cmd))
except Exception as ex:
print(ex)
prompt()
print()
Mai jos funcția split_cmd returnează un tuplu cu două valori care sunt despachetate de către apelator. În continuare se face o căutare în dicționarul CMD_MAP după numele comenzii. Dacă acesta e găsit în CMD_MAP se obține funcția care e apelată cu parametrii comenzii și se returnează valoarea rezultată.
def execute(cmd):
def split_cmd():
cmd_parts = cmd.split()
return cmd_parts[0], cmd_parts[1:]
command_name, params = split_cmd()
if command_name in CMD_MAP:
command_function = CMD_MAP[command_name]
return command_function (*params)
else:
raise Exception("Bad command: %s" % command_name)
Funcția ls primește *args ceea ce înseamnă că variabila args este un tuplu și va conține un număr variabil de elemente, care vor fi parametrii cu care a fost apelata funcția.
def ls(*args):
from os import listdir
if not args:
return ls(".")
elif len(args) > 1:
raise Exception("Too many parameters")
else:
dir_content = listdir(args[0])
return "\n".join(dir_content)
Funcția join a clasei string, care e apelată mai sus, unește o listă de stringuri folosind un separator dat. Exemplu: ",".join(["a", "b", "c"]) va returna "a,b,c"
def cat(*path_list):
if not path_list:
raise Exception("No files to cat.")
def cat_one_file(path):
try:
with open(path) as f:
content = f.read()
return "%s:\n%s" % (path, content)
except:
return "Could not open file '%s'" % path
contents = [cat_one_file(path) for path in path_list]
return "\n".join(contents)
CMD_MAP = {"ls": ls, "cat": cat}
Mai sus CMD_MAP este un dicționar în care cu cheia "ls" e asociata funcția ls iar cu cheia "cat" e asociată funcția cat. Se vede că funcțiile sunt folosite ca și niște variabile normale. În momentul în care sunt apelate se folosește sintaxa "nume_functie(param1, param2, param3)".
try:
main()
except KeyboardInterrupt:
print()
exit(1)
Codul de mai sus face apelul la funcția main și tratează Ctrl+C și returnează codul 1 dacă s-a ieșit cu Ctrl+C. Faptul că l-am pus în afara funcției main e doar o chestie de preferințe. Eu prefer sț lucrez așa pentru că funcția exit închide programul imediat și prefer să limitez folosirea lui exit în afara funcției main
Codul Haskell a fost scris folosind "Glasgow Haskell Compilator" GHC 7.0.3 pe Ubuntu Linux. Acesta se poate instala foarte ușor folosind comanda
$sudo apt-get install haskell-platform
...iar ca editor am folosit Geany care se instalează
$sudo apt-get install geany20
Codul Haskell rezultat conține cca 1200 caractere împărțite pe 45 linii de cod. În continuare urmează codul, pe care îl voi explica unde voi considera că e nevoie.
Pentru a genera un executabil compilatorul are nevoie de un modul Main care să conțină o funcție main:
module Main where
import System.IO
import Data.List
import Data.String.Utils
import System.Directory
Aceasta e funcția main, care are implicit tipul "IO () adica nu returnează nimic ci doar produce efecte secundare de tip IO".
După cum se vede funcția este o înșiruire de acţiuni, separate prin unul dintre operatorii >> sau >>=. Diferența intre cei doi operatori este că >>= ia rezultatul acțiunii anterioare și îl pasează acțiunii următoare ca și parametru de intrare, în stil "pipeline". Deci mai jos spunem că funcția main se obține prin compunerea funcțiilor următoare, putStrLn, prompt, getLines, mapM_, putStrLn. Având în vedere că acestea sunt "actiuni", deci produc efecte secundare, operatorii >> respectiv >>= le compun astfel încât să țină seama de ordinea de execuție.
O funcție interesantă care apare în main este funcția getLines. Ea nu primește zero parametrii și returnează o listă de linii citite de la tastatură. E foarte important faptul ca, că majoritatea funcțiilor din haskell, sunt funcții "lazy". Aceasta înseamnă că nu așteaptă să citeasca toate liniile din stdin ci fiecare linie citita e dată mai departe pentru procesare.
Ar mai fi de comentat asupra funcției mapM_. Aceasta este o funcție din familia map și primește doi parametrii: o acțiune și o listă de obiecte. Acțiunea este executată pentru fiecare element din listă. Primul parametru al funcției mapM este funcția process iar al doilea este listă de linii returnată de getLines.
main = putStrLn "Ctrl+C to exit" >>
prompt >>
getLines >>=
mapM_ process >>
putStrLn ""
Mai jos e declarată funcția process ca fiind o compunere de funcții. Sintaxa run.word.strip, exact ca și compunerea din matematică, înseamnă că funcția strip se apelează cu un parametru de intrare , funcția words se apelează cu rezultatul returnat de strip iar apoi funcția run se apelează cu rezultatul returnat de words.
Funcția process nu are nici un tip declarat, dar fiind o functie compusă tipul se poate deduce ușor. Primește la intrare aceiași parametrii ca funcția strip și returnează ce returnează funcția run. În concluzie process primește un string și nu returnează nici o valoare pentru ca run are tipul "IO ()". În schimb știm că are un efect de tip IO. Deci semnătura funcției process este "process :: String -> IO ()". În cod, pentru ca nu am considerat că e nevoie de semnatură pentru a înțelege funcția am ales să nu o declar și să las compilatorul să facă inferența de tipuri.
process = run.words.strip
Funcția getLines se compune oarecum asemănător cu funcția main. Diferența este că de data aceasta avem funcția lines, care e o funcție pură, pe care o apelăm peste rezultatul unei acţiuni, funcția getLines. Funcția getContents, fiind o funcție cu efecte de tip IO se execută "în monada IO". Rezultatul ei nu poate fi pasat funcției lines pentru că aceasta este în afara monadei IO, fiind o funcție pura. Pentru a putea face apelul compunem funcția lines cu funcția return, obtinând astfel o funcție în monada IO care poate primi rezultatul funcției getContents.
getLines = getContents >>= (return.lines)
prompt = putStr ">" >> hFlush stdout
După cum se vede pentru funcția run am considerat că semnatura ei ar face codul mai ușor lizibil așa că funcția e insoțită de declarația de tip.
run :: [String] -> IO ()
run cmd = execute cmd >>= putStrLn >> prompt
Funcția joinLines, după cum se vede primește un parametru de tip listă de String și returnează un string. Totuși în declarația functiei nu apare nici un parametru. Motivul este faptul că funcția join primește doi parametrii. Primul dintre ei este separatorul și al doilea este lista de șiruri care sunt alăturate. Daca join este apelată partial cu un singur parametru obținem o funcție care așteaptă al doilea parametru și apoi face apelul efectiv la join.
joinLines :: [String] -> String
joinLines = join "\n"
Mai jos e definita funcția "execute" printr-o serie de ecuații care acoperă diverse condiții de apelare a funcției. Sintaxa aceasta se poate folosi ca alternativă la instructiunea if. Așadar, dacă parametrul apelat se potrivește cu unul dintre cele 3 pattern-uri se va executa ecuația corespunzătoare. După cum se vede, avem 3 cazuri: în care primul element din lista este "ls", situația în care primul element este "cat" și restul.
execute :: [String] -> IO String
execute ("ls":params) = ls params
execute ("cat":params) = cat params
execute _other = return "Bad command"
Funcția ls se definește la fel ca execuție, cu mai multe ecuaţiile. Cazul în care e apelată cu o lista goală, cazul în care e apelată cu o lista cu un singur element și cazul în care e apelată cu mai mult de un element. Pentru că ecuaţiile sunt încercate în ordine ecuaţia a treia este executată doar când celelalte doua nu s-au potrivit.
Ar mai fi de notat funcția catch care primeste doi parametrii: o functie de executat și un handler de excepții. Ea executa funcția și în caz ca s-a aruncat o eroare de tip IOError execută handlerul. În cazul nostru handlerul este o funcție anonimă, numita și "funcție lambda". Sintaxa pentru definirea funcțiilor lambda este "\ param1, param2, ...,paramN -> cod". Deoarece nu ne interesează ce excepție s-a prins parametrul de intrare este _
ls :: [String] -> IO String
ls [] = ls ["."]
ls [dir] = catch (getDirectoryContents dir >>= return.joinLines.sort)
(\_ -> return $ "Could not read '" ++ dir ++ "'.")
ls (dir:extraParams) = return "Too many parameters."
În funcţia cat, fiind mai complexă avem nevoie sa definim nişte funcţii locale sau funcţii "inner". Acestea sunt definite în clauza where. Blocul where este delimitat tot prin indentare. După cum se vede și funcţiile inner pot avea la rândul lor blocuri where.
Funcţiile inner au acces la toate numele definite în namespace-ul funcţiei părinte. Astfel funcţia formatOutput are acces la parametrul path al funcţiei părinte catOneFile. De asemenea ar putea avea acces și la parametrul files al funcţiei bunic cat la nevoie.
cat :: [String] -> IO String
cat [] = return "No files to cat."
cat files = mapM catOneFile files >>= return.joinLines
where
catOneFile :: String -> IO String
catOneFile path = catch (readFile path >>= return.formatOutput)
(\_ -> return $ "Could not open file '" ++ path ++ "'.")
where
formatOutput content = path ++ ":\n" ++ content
Aștept ca cititorii să trimită părerea lor pe email la ovidiu.deac@todaysoftmag.ro, să comenteze și să voteze pentru fiecare soluție cu unul dintre cele trei tipuri de voturi: good, bad, ugly.
de Ovidiu Deac
de Flaviu Mățan
de Ioana Fane