Kiedy przeglądamy kod, który staje się zbyt złożony i trudny do rozszerzenia, jednym z głównych rozwiązań jest jego refaktoryzacja. Chodzi tu o zmianę struktury kodu bez modyfikowania jego działania. W tym kontekście refaktoryzacja umożliwia uproszczenie logiki, poprawę czytelności i łatwiejsze dodawanie nowych funkcjonalności w przyszłości. Proces ten jest szczególnie istotny, gdy kod wchodzi w fazę, w której konieczne jest dodanie nowych kryteriów wyboru, ale istniejące podejście staje się nieczytelne i nieefektywne.
W przypadku kodu przedstawionego w przykładowym zadaniu, zauważamy nadmiar wcięć i złożoność wynikającą z użycia wielu operacji logicznych oraz nawiasów. Każda zmiana w tym kodzie niesie ze sobą ryzyko wprowadzenia błędów, a sam proces rozbudowy programu staje się niepokojąco trudny. Chcielibyśmy więc uprościć kod i jednocześnie utrzymać jego funkcjonalność, korzystając z możliwości, które oferuje język Rust, w tym jego systemu iteracji i filtracji.
W tym celu zastosujemy funkcję Iterator::filter, która umożliwia selekcję elementów na podstawie określonych kryteriów, minimalizując złożoność i zwiększając przejrzystość kodu. Z pomocą tej funkcji możemy na przykład filtrwać wpisy według ich typu lub nazwy. Poniżej przedstawiam przykład, w którym stosujemy takie podejście.
Przykładowy kod wykonuje filtrowanie plików i katalogów, przeprowadzając operacje na dwóch głównych parametrach: typie pliku oraz nazwie. Aby osiągnąć zamierzony efekt, używamy dwóch zamknięć (closures), które sprawdzają, czy dany wpis spełnia warunki filtracji.
Pierwsze zamknięcie dotyczy typu pliku i sprawdza, czy plik jest dowiązaniem (symlinkiem), katalogiem lub zwykłym plikiem. Jeśli żadne z tych typów nie zostało określone przez użytkownika, kod zwraca prawdę. W przeciwnym przypadku iteruje po dostępnych typach i porównuje je z typem pliku:
Drugie zamknięcie działa na nazwach plików, filtrując tylko te, które pasują do przynajmniej jednego z podanych wyrażeń regularnych. Jeżeli lista nazw jest pusta, kod zwróci prawdę, akceptując wszystkie pliki:
Wszystkie operacje filtracyjne mogą zostać połączone w jeden łańcuch operacji na iteratorze, gdzie filter_map umożliwia obsługę błędów wejściowych, a kolejne operacje filter i map pozwalają na wyodrębnienie i przetworzenie wyników.
Dzięki zastosowaniu Iterator::filter i Iterator::map kod staje się bardziej przejrzysty, a dodawanie nowych filtrów staje się o wiele prostsze. Na przykład, łatwo możemy dodać nowy filtr według rozmiaru pliku lub daty ostatniej modyfikacji, co w poprzedniej wersji kodu mogło wymagać większych zmian.
Zaletą tego podejścia jest również bezpieczeństwo kompilacji, które zapewnia system typów w Rust. Dzięki temu, używając typu EntryType, możemy mieć pewność, że wszystkie możliwe warianty tego typu są odpowiednio obsłużone, a kompilator zwróci błąd, jeśli jakiś przypadek zostanie pominięty. Na przykład, jeśli w jednej z wersji kodu usuniemy przypadek obsługi pliku, kompilator natychmiast poinformuje nas o tym błędzie:
Zmiana struktury kodu przy pomocy refaktoryzacji i użycie iteracji w Rust to kluczowe elementy pozwalające na utrzymanie wysokiej jakości kodu, zwłaszcza gdy projekt staje się coraz bardziej rozbudowany.
Warto również zauważyć, że kod taki może być wykorzystywany zarówno na systemach Unix, jak i Windows, jednak w przypadku Windows istnieją pewne różnice, takie jak traktowanie dowiązań symbolicznych jako zwykłych plików. W związku z tym, podczas pisania testów ważne jest, aby uwzględnić te różnice i przygotować odpowiednie przypadki testowe, które będą działać na obu platformach.
Testowanie wymaga także odpowiedniej obsługi różnych wyników w zależności od systemu operacyjnego, na którym uruchamiany jest program. W tym celu pomocna jest funkcja run, która uruchamia program z różnymi argumentami i porównuje wynik z oczekiwanym wyjściem zapisanym w pliku. Jest to sposób, który zapewnia pewność, że zmiany w kodzie nie wprowadziły nowych błędów:
Dzięki takim praktykom możemy utrzymać wysoką jakość kodu, zapewniając jego łatwą rozbudowę i utrzymanie.
Jak zaimplementować argumenty w programie w języku Rust?
Podczas tworzenia programu w języku Rust, który naśladuje działanie klasycznego narzędzia cut, ważnym krokiem jest odpowiednie zaimplementowanie obsługi argumentów. Program ten wymaga definiowania i walidacji danych wejściowych, takich jak pliki do przetworzenia, delimitery pól, czy też zakresy do wycięcia. Użycie biblioteki clap jest jednym z najwygodniejszych sposobów do realizacji tego zadania, a poniżej przedstawiamy kroki, jak skonstruować odpowiednią obsługę argumentów.
Przede wszystkim, do realizacji projektu wymagane są pewne zewnętrzne zależności, które należy dodać do pliku Cargo.toml:
Zależność csv będzie używana do przetwarzania plików o formacie CSV, a clap będzie odpowiedzialna za definiowanie i walidowanie argumentów programu. Po dodaniu tych zależności, kolejnym krokiem jest uruchomienie testów, które, na początku, nie powinny przejść – ponieważ nie zaimplementowaliśmy jeszcze żadnej logiki.
Definiowanie argumentów programu
Program będzie obsługiwał trzy kluczowe opcje, z których użytkownik musi wybrać jedną, a mianowicie:
-
--fields(lub-f) – do określenia wybranych pól, -
--bytes(lub-b) – do określenia wybranych bajtów, -
--chars(lub-c) – do określenia wybranych znaków.
Korzystając z biblioteki clap, możemy zdefiniować strukturę, która będzie zawierała te argumenty. Do implementacji użyjemy struktury Args, która przechowa pliki wejściowe, separator, oraz informacje o tym, które dane mają zostać wycięte.
Warto zauważyć, że każda z opcji (fields, bytes, chars) jest opcjonalna, ale użytkownik może wybrać tylko jedną z nich. Grupa tych argumentów (ArgGroup) powinna być zdefiniowana w taki sposób, aby wymusić wybór dokładnie jednej z opcji.
Konfiguracja clap i parsowanie argumentów
Aby skonfigurować clap w wersji opartej na derive, stworzymy odpowiednią strukturę, która automatycznie utworzy wymagany interfejs wiersza poleceń. Użyjemy do tego ArgsExtract, który będzie zawierał szczegóły dotyczące tego, co chcemy wyciąć. Warto także zaznaczyć, że wszystkie te opcje mogą przyjmować wartości domyślne, takie jak separator tabulatora (\t) dla -d lub -f.
Użycie #[group] pozwala na wymuszenie, by tylko jedna z opcji fields, bytes lub chars była użyta jednocześnie. #[command(flatten)] natomiast umożliwia "spłaszczenie" zagnieżdżonej struktury ArgsExtract do głównej struktury Args.
Walidacja argumentów
Po poprawnym skonfigurowaniu argumentów, ważnym krokiem jest ich walidacja. Jeśli użytkownik nie poda wymaganej opcji fields, bytes lub chars, program powinien zwrócić odpowiedni błąd. Program może także przeprowadzić dodatkowe sprawdzenie, np. czy separator został prawidłowo ustawiony, czy pliki wejściowe są dostępne, a także czy wybrany typ danych (pola, bajty, znaki) nie koliduje ze sobą.
Przykład uruchomienia programu z poprawnymi danymi:
Jeśli użytkownik poda dwa konflikujące argumenty, np. -f i -b, program zwróci błąd:
Zakończenie implementacji
Ostatecznie, ważnym krokiem jest zbudowanie testów, które sprawdzą poprawność działania programu. Testowanie takich aplikacji jest niezbędnym elementem zapewnienia stabilności, zwłaszcza gdy program zaczyna obsługiwać pliki w różnych formatach. Testy powinny obejmować zarówno poprawne, jak i błędne przypadki, aby upewnić się, że program działa zgodnie z oczekiwaniami w każdej sytuacji.
Endtext
Jak wykorzystać narzędzie comm do porównywania plików?
Narzędzie comm w systemach Unix jest jednym z najprostszych, ale zarazem bardzo użytecznych narzędzi do porównywania dwóch posortowanych plików tekstowych. Jego działanie opiera się na porównaniu linii w dwóch plikach, prezentując wyniki w trzech kolumnach: linie unikalne dla pierwszego pliku, linie unikalne dla drugiego pliku oraz linie wspólne dla obu plików. Istnieje jednak wiele dodatkowych opcji, które pozwalają na dokładniejsze dostosowanie wyników porównania, takich jak możliwość ignorowania wielkości liter, a także możliwość zmiany separatora kolumn.
Podstawowe opcje komendy comm obejmują:
-
-1– tłumienie pierwszej kolumny, czyli linii unikalnych dla pierwszego pliku, -
-2– tłumienie drugiej kolumny, czyli linii unikalnych dla drugiego pliku, -
-3– tłumienie trzeciej kolumny, czyli linii wspólnych, -
-i– porównanie niezależne od wielkości liter, -
--output-delimiter=STR– określenie niestandardowego separatora między kolumnami (domyślnie tabulator), -
-z– użycie zakończenia linii w postaci NUL zamiast nowej linii.
Komenda comm zakłada, że porównywane pliki są posortowane, ponieważ porównanie jest przeprowadzane w oparciu o lexiczną kolejność linii. Ważne jest również, aby przed użyciem tego narzędzia odpowiednio posortować pliki, co w przypadku dużych zbiorów danych jest szczególnie istotne, aby uzyskać poprawne wyniki.
Przykłady zastosowania
Weźmy przykład z życia codziennego: mamy dwa pliki zawierające listy miast, w których odbyły się koncerty naszego ulubionego zespołu. Pierwszy plik zawiera miasta z poprzedniej trasy, a drugi – z obecnej. Aby znaleźć miasta, które zostały odwiedzone podczas obu tras, możemy użyć komendy comm z opcją -12, co oznacza, że chcemy wyświetlić tylko wspólne linie (miasta, które pojawiły się w obu plikach). Przykład:
Wynikiem będą miasta, które pojawiły się w obu trasach, np.:
Jeśli natomiast chcemy znaleźć tylko te miasta, które zostały odwiedzone wyłącznie podczas pierwszej trasy, należy użyć opcji -23, która tłumi drugą i trzecią kolumnę:
Wynikiem będą miasta, które zostały odwiedzone wyłącznie w pierwszej trasie, np.:
Z kolei, aby znaleźć miasta, które pojawiły się tylko w drugiej trasie, użyjemy opcji -13, która tłumi pierwszą i trzecią kolumnę:
Wynikiem będą miasta, które zostały odwiedzone wyłącznie w drugiej trasie:
Zastosowanie w bioinformatyce
Choć przykład z miastami może wydawać się trywialny, narzędzie comm znajduje swoje miejsce także w bardziej zaawansowanych dziedzinach, takich jak bioinformatyka. W tej dziedzinie często porównuje się duże zbiory danych, np. sekwencje białek, które zostały pogrupowane w różne klastry. Przy pomocy comm możemy porównać sekwencje białek, które zostały pogrupowane w klastrach, z oryginalną listą sekwencji, aby znaleźć te, które nie zostały pogrupowane. Tego typu analiza pozwala na wykrycie potencjalnych białek o unikalnych właściwościach, które mogą wymagać dalszego badania.
Opcja porównania bez uwzględniania wielkości liter
W wersji BSD narzędzia comm istnieje możliwość przeprowadzenia porównań, które ignorują wielkość liter, za pomocą opcji -i. Działa to na przykład w przypadkach, gdy chcemy porównać teksty, które mogą zawierać różne zapisy liter (małe/duże litery), ale same treści są takie same. Na przykład:
Wynikiem będą miasta, które występują w obu plikach, niezależnie od tego, czy są zapisane małymi, czy dużymi literami.
Wskazówki do implementacji wersji Rust
Wersja narzędzia comm w języku Rust, nazwana commr, może być tworzona przy użyciu popularnych bibliotek, takich jak clap do obsługi argumentów wiersza poleceń i anyhow do obsługi błędów. Podstawowa struktura argumentów powinna zawierać informacje o dwóch plikach wejściowych, możliwościach tłumienia kolumn oraz ewentualnych opcjach takich jak porównanie niezależne od wielkości liter. Implementacja może zaczynać się od zdefiniowania struktury Args i obsługi odpowiednich argumentów w funkcji get_args.
Co warto dodać?
Ważnym elementem korzystania z narzędzi porównujących pliki jest świadomość, że dla poprawności wyników kluczowe jest prawidłowe przygotowanie danych wejściowych – muszą być one odpowiednio posortowane. W przypadku dużych zbiorów danych może się okazać, że dodatkowe optymalizacje, takie jak efektywne zarządzanie pamięcią podczas sortowania, będą miały kluczowe znaczenie. Również znajomość różnych opcji dostosowujących sposób porównywania, takich jak uwzględnianie wielkości liter czy zmiana separatorów, może pomóc w precyzyjnym dostosowaniu wyników analizy do indywidualnych potrzeb. Praktyczne zastosowanie komendy comm wykracza poza proste porównania tekstów, mając szerokie zastosowanie w analizach biologicznych, porównywaniu wyników badań oraz optymalizacji procesów przetwarzania danych w naukach przyrodniczych.
Jak zbudować program w Rust z obsługą argumentów, losowym wyborem i wzorcami
Tworzenie programu w Rust, który przetwarza argumenty wiersza poleceń, obsługuje losowe wybory na podstawie zadanych nasion i używa wyrażeń regularnych, może wydawać się skomplikowane, ale dzięki odpowiednim narzędziom, takim jak biblioteka clap, jest to stosunkowo proste. Program "fortuner" – wersja w Rust oryginalnej aplikacji fortune – jest doskonałym przykładem na wykorzystanie tych technik.
Zaczynamy od utworzenia nowego projektu w Rust za pomocą komendy:
Następnie dodajemy do pliku Cargo.toml odpowiednie zależności:
Po skonfigurowaniu zależności, kopiujemy katalog 12_fortuner/tests z książki do naszego projektu i uruchamiamy testy:
Wszystkie testy powinny początkowo zakończyć się niepowodzeniem, ale to tylko część procesu tworzenia aplikacji. Celem jest teraz zdefiniowanie argumentów, które program będzie przyjmował.
Definicja Argumentów
Aby zdefiniować argumenty w naszym programie, zaczynamy od stworzenia struktury Args:
Argument sources to lista plików lub katalogów, które będą przetwarzane przez program. Argument pattern to opcjonalny wzorzec, który będzie filtrował wyniki, insensitive określa, czy dopasowanie powinno być niewrażliwe na wielkość liter, a seed to nasiono dla generatora liczb losowych, które pozwala na powtarzalne wybory.
W pliku main.rs definiujemy funkcję get_args(), która przy pomocy biblioteki clap będzie analizować argumenty wiersza poleceń:
Teraz możemy wywołać funkcję get_args() w main() i wydrukować przekazane argumenty:
Wynik powinien wyglądać mniej więcej tak:
Program powinien wymagać przynajmniej jednego pliku lub katalogu jako argumentu. Jeśli nie zostanie podany, program powinien zakończyć działanie, wyświetlając komunikat o błędzie:
Walidacja Argumentów
Należy zadbać o prawidłową interpretację argumentów. Na przykład, -m (wzorzec) powinien przyjmować wartość typu String, a -s (seed) powinno być typu u64. Jeśli --seed zawiera nieprawidłową wartość, program powinien zgłosić błąd:
Generowanie Liczb Losowych
W tej aplikacji, aby uzyskać "losowy" wybór, używamy pseudolosowego generatora liczb (PRNG). Generowanie liczb losowych w komputerze nie jest rzeczywiście losowe, ale deterministyczne, co oznacza, że przy tym samym nasieniu zawsze uzyskamy te same wyniki. To właśnie daje możliwość testowania pseudolosowych programów – można używać tego samego nasienia do uzyskania przewidywalnych wyników. W tym przypadku wykorzystujemy bibliotekę rand do tworzenia PRNG.
Nasiono (seed) możemy przekazać przez argument -s i używać go do generowania tych "losowych" wyborów. Jeśli nasiono nie jest podane, program będzie korzystać z innego źródła losowości, dzięki czemu wybory będą wydawać się przypadkowe.
Obsługa Wyrażeń Regularnych
Kolejną istotną częścią jest obsługa wzorców za pomocą wyrażeń regularnych. Dzięki wyrażeniom regularnym możemy filtrować dane na podstawie różnych kryteriów. Aby umożliwić niewrażliwość na wielkość liter, używamy regex::RegexBuilder z opcją case_insensitive, która sprawia, że dopasowania są ignorujące wielkość liter.
W funkcji run możemy tworzyć wyrażenie regularne na podstawie podanego wzorca:
Wartość args.pattern jest opcjonalna, więc musimy odpowiednio obsłużyć przypadek, w którym użytkownik jej nie podał. Jeśli podano niewłaściwy wzorzec, należy zgłosić błąd.
Ważne Aspekty, Które Należy Zrozumieć
Tworzenie programu, który operuje na plikach, używa wyrażeń regularnych i generuje pseudolosowe wyniki, wymaga uwagi na kilka kluczowych kwestii:
-
Dokładność Argumentów – Każdy argument powinien być odpowiednio walidowany, zwłaszcza przy przekazywaniu wartości, takich jak nasiono, które musi być liczbą całkowitą. Niewłaściwe dane wejściowe, jak np. niepoprawny typ, mogą prowadzić do błędów w działaniu programu.
-
Zrozumienie PRNG – Warto wiedzieć, że generator liczb losowych w komputerze nie jest naprawdę losowy. Przy takim podejściu należy zachować ostrożność, zwłaszcza w kontekście testowania oprogramowania, ponieważ ten sam seed zawsze wygeneruje te same wyniki.
-
Obsługa Wyrażeń Regularnych – Dobre zrozumienie wyrażeń regularnych jest kluczowe, szczególnie gdy chcemy dodać elastyczność do aplikacji, pozwalając użytkownikowi na dopasowanie wzorców do tekstu w różnych formach.
-
Testowanie – Aby upewnić się, że aplikacja działa prawidłowo, warto dodać odpowiednie testy jednostkowe, które sprawdzą, czy poprawnie obsługujemy różne przypadki, takie jak błędne dane wejściowe czy brak argumentów.
Czym jest elektrochromizm i jakie ma znaczenie w nowoczesnych materiałach funkcjonalnych?
Jakie technologie wykorzystuje się do wytwarzania biopolimerowych hydrogeli i bioplastików do zastosowań inżynierii tkankowej?

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский