რა ვუყოთ ქართულ მთავრულ ასოებს უნიკოდში?

შარშან მაისში Unicode-მა ქართული მხედრული ანბანის 46 მთავრული ასონიშანი დაამტკიცა.

შეიძლება ჯერ კიდევ ადრეა, მაგრამ მომავალში ოპერაციული სისტემების განახლებებში მაინც აისახება ეს ცვლილება. ცნობისმოყვარეობის გამო გადავწყვიტე პატარა მოკვლევა გამეკეთებინა, თუ რას გვიცვლის ეს ამბები პროგრამისტებს და ამ პოსტში გაგიზიარებთ.

მანამდე ყოველი შემთხვევისთვის რამდენიმე განმარტება:
Unicode – სტანდარტი, რომელიც ყველა სიმბოლოს უნიკალური რიცხვითი კოდით განსაზღვრავს. ასევე აღწერს სხვადასხვა ენისთვის საჭირო სპეციფიურ წესებს. ამ სტანდარტმა მთელი ტექნიკური სამყარო მოიცვა და დღეს ყველა იყენებს მას, ვისაც ტექსტის დამუშავება / გამოსახვა სჭირდება – ოპერაციული სისტემები, პლატფორმები, ბრაუზერები…

UTF-8 – უნიკოდს სიმბოლოების და რიცხვების სია აქვს, თუმცა არ ანაღვლებს, როგორ მოხდება ამ ინფორმაციის შენახვა მეხსიერებაში. ამისთვის კოდირების სხვადასხვა ალგორითმი არსებობს. UTF-8 ერთ-ერთი და ყველაზე გავრცელებული მათგანია, რადგან ოპტიმალურად იყენებს ადგილს და ზედმეტ ბაიტებს არ გამოყოფს სიმბოლოსთვის, თუ ის ერთ ცალშიც ეტევა. კოდირების სხვა მაგალითებია UCS-2, UTF-16, UTF-32…

სტანდარტში ცვლილებები იმპლემენტაციებში ცვლილებებს იწვევს, რაც არც ისე სწრაფად ხდება. მაგალითად, ₾ ლარის სიმბოლო უნიკოდის მერვე ვერსიაში დაემატა 2015 წლის 17 მაისს და ვინდოუსის განახლება ამ სიმბოლოს ასახვისთვის 2016 წლის 19 იანვარს გამოვიდა.

ოპერაციულებმა უნდა განაახლონ კლავიატურის დრაივერი, რომ მომხმარებლებს CAPS რეჟიმის ჩართვით შეეძლოთ მთავრულის წერა. და ასევე უნდა განაახლონ სისტემური ფონტები, რომ font fallback-ის დროს სწორი სიმბოლოები გამოჩნდეს.

იმის გამო, რომ მთავრულის და არამთავრულის ერთსა და იმავე სიმბოლოს სხვადასხვა კოდი აქვს, პროგრამისტებს რაღაცების გათვალისწინება გვიწევს ხოლმე – აქამდე მხოლოდ სხვა ენებისთვის, ახლა კი ქართულისთვისაც. მაგალითად, როცა ხდება სტრიქონების შედარება, ძებნა, regex პატერნები, სორტირება, ბაზაში შენახვა და ა.შ.

 

მონაცემთა ბაზა

MS SQL server-ს ჩაშენებული აქვს უნიკოდის მხარდაჭერა და ოპერაციებისას ისედაც სტანდარტს უყურებს, მთავარია სწორ ვერსიას უყუროს: SQL Fiddle

აი, MySQL-თან ცოტა სხვაგვარადაა – აქ თითოეულ ბაზას, ცხრილს ან საერთოდ ველს შეიძლება ჰქონდეს განსაზღვრული კონკრეტული collation (წყობა?), იმის მიხედვით თუ რა ტიპის ინფორმაციას ინახავს. ჩვენ დაჩვეულები ვართ, რომ utf8_general_ci გამოვიყენოთ, რომელსაც ქართული ასოებიც ‘ესმის’. ეს collation სრულყოფილად არ აკეთებს უნიკოდის იმპლემენტაციას utf8_unicode_ci-ისგან განსხვავებით. უბრალოდ მეტი წარმადობისთვის იყენებდნენ. ახლანდელი პროცესორების პირობებში დიდი სხვაობა აღარ არის. სამაგიეროდ, utf8_unicode_ci ქართულ წყობას კარგად გაიგებს.

აი, მაგალითს მოვიყვან:
უნიკოდს კოდებთან ერთად განსაზღვრული აქვს სიმბოლოების მიმდევრობაც, რომელსაც სორტირებისას ვიყენებთ. მაგალითად ამ სიაში ყველა ქართული ‘ა’ სიმბოლო მიყოლებით არის – ნუსხურიც, ასომთავრულიც და მხედრულიც. მათ შემდეგ მოდის ‘ბ’ ასოს ვარიანტები. სავარაუდოდ ახალი მთავრულიც ანალოგიურად დაემატება.

SQL Fiddle

CREATE TABLE IF NOT EXISTS `test` (
  `content` varchar(200) NOT NULL
) DEFAULT CHARSET=utf8 COLLATE utf8_general_ci;
INSERT INTO `test` (`content`) VALUES
  ('აბგ'),  ('ააააა'),  ('Ⴁააააა'),  ('Ⴀააააა'),  ('bcd'),  ('ab.'),  ('Ⴄ'),  ('ж'),  ('Ж'),  ('ц'),  ('Ц');
  

CREATE TABLE IF NOT EXISTS `test_better` (
  `content` varchar(200) NOT NULL
) DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci;
INSERT INTO `test_better` (`content`) VALUES
  ('აბგ'),  ('ააააა'),  ('Ⴁააააა'),  ('Ⴀააააა'),  ('bcd'),  ('ab.'),  ('Ⴄ'),  ('ж'),  ('Ж'),  ('ц'),  ('Ц');


select * from `test` d order by d.content;
select * from `test_better` d order by d.content;

შედეგი:

ab., bcd, Ж, ж, ц, Ц, Ⴀააააა, Ⴁააააა, Ⴄ, ააააა, აბგ
ab., bcd, ж, Ж, ц, Ц, ააააა, Ⴀააააა, აბგ, Ⴁააააა, Ⴄ

ახლა რომ MySQL 8-ის ბეტა რელიზი გამოვიდა, იქ ჯერ უნიკოდის მეცხე ვერსიაა. ჩვენი მთავრული ასოები კი მეთერთემეტეშია 🙂

 

ჯავასკრიპტი

მართალია ბევრი რეალიზაცია არსებობს, მაგრამ V8-ს ვერ ავცდებით, ამიტო მაგის მიხედვით განვიხილავ.

ჯავასკრიპტს უნიკოდის მხარდაჭერა კი აქვს, თუმცა რაღაცები მაინც პრობლემურია (მაგალითად რეგექსი). თუ საიტზე გვჭირდება სორტირება ან შედარება (მაგალითად თუ ფილტრები გვაქვს), მაშინ ჩვეულებრივი სორტირება არ გამოგვადგება და Locale უნდა მივუთითოთ, რომ უნიკოდის წესები გათვალისწინოს:
მაგალითად,

let a = ['აბგ','ააააა','Ⴁააააა','Ⴀააააა','bcd','ab.','Ⴄ','ж','Ж','ц','Ц'];
console.log(a.sort());
console.log(a.sort(Intl.Collator('ru').compare));

ქართული Collation-ის მხარდაჭერა სამწუხაროდ საერთოდ არ აქვს. ამიტომ ნუსხურთან და ასომთავრულთან ერთად სწორად ვერ დაასორტირებს. ნუ, ეს მაინც ძალიან იშვიათი შემთხვევაა სადარდებლად რომ ღირდეს. სტანდარტულად, code point-ების მიხედვით ალაგებს და ანბანის მიხედვით გამოვა, მხოლოდ მთავრულ “ა” ასოს ჩვეულებრივი “ჰ”-ს მერე დასვამს ან პირიქით.

ამ პრობლემას სხვა ენებში სტრიქონების ერთ რეგისტრში გადაყვანით ვჭრით. გიორგიმ იდეა მომაწოდა:

myArray.sort(function(s1,s2){ return s1.toLowerCase() > s2.toLowerCase()}));

ქართულზეც სავარაუდოდ ეგრე იმუშავებს, მას შემდეგ რაც V8-ში უნიკოდის იმპლემენტაცია განახლდება. ახლა მაგალითად ასომთავრულზე და ნუსხურზე ეგრეა: "Ⴀ".toLowerCase() => "ⴀ"

როგორც ჩანს, იმის გამო რომ სტანდარტში ასომთავრულს CAPITAL აწერია, ხოლო ნუსხურს SMALL, ის ნუსხურის მთავრულ ვარიანტად არის იმპლემენტირებული (ეს v8 source ფაილი: unicode.cc, კოდებია პირდაპირ შესაბამებული.)
მხედრული ახლა caseless არის. საინტერესოა როგორი აღნიშნვა ექნება. მგონი სხვა ენა არ არსებობს, რომელსაც ორნაირი მთავრული ასოები აქვს.
Anyway, ამასაც ვერსიის განახლება სჭირდება.

ახლა გამახსენდა, რომ V8 საერთოდ open source პროექტია და მოხალისეს შეუძლია საერთოდ ქართული locale დაამატოს. ჯერ-ჯერობით ამის შედეგი ცარიელია:

Intl.Collator.supportedLocalesOf('ka')

 

ჯავა

ჯავაც არ ჩქარობს განახლებებს. JDK 9 უნიკოდის მერვე ვერსიით (სადაც ლარის სიმბოლო დაემატა) ორი წლის შემდეგ – 2017 წლის სექტემბერში გამოვიდა.
აქ სტრიქონების შედარება ხდება equals მეთოდით. მომავალში ქართულისთვისაც მოგვიწევს რომ equalsIgnoreCase გამოვიყენოთ:

"Ⴀ".equals("ⴀ")  => false
"Ⴀ".equalsIgnoreCase("ⴀ")  => true

ერთი მთავრული რადგან უკვე გვაქვს, მაგის გამოყენებით ვტესტავ, უბრალოდ პრაქტიკაში არ ვიყენებთ მაგ ასოებს.

ასევე რეგექსის დროსაც პირდაპირ ვერ მოვძებნით. ჩვეულებრივი i – ignore case არ გამოგვადგება, უნიკოდს სხვანაირად ამუშავებს. ამიტო:

"A".matches("(?i)[a]")  => true
"Ⴀ".matches("(?i)[ⴀ]") => false

Pattern.compile("[ⴀ]", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE).matcher("Ⴀ").matches();  => true

შესაბამისად ყველგან სადაც სადაც სტრიქონებს ვიყენებთ, გათვალისწინება იქნება საჭირო – maps, sets, etc.

 

PHP

აქ საერთოდ უნიკოდთან ცუდი სამუშაოა + დაემატება აქაც გადაყვანა-გადმოყვანა.

 

 
კიდევ ცვლილება დაგვჭირდება ერთ ადგილას – ძებნის ძალიან მოსახერხებელ ხელსაწყოებში – grep და მისთანები. grep-ის case insensitive ოპცია ახლაც არ მუშაობს ქართულ ასომთავრულ ასოებზე. იმედია მაგათ ვერსიების განახლებაშიც აისახება, უებარი პროგრამებია დიდ (და პატარაც) ტექსტებში და ფაილებში რეგექსით საძებნად.

ბევრი აქაური სისტემა მაშინვე ვერც განაახლებს ალბათ პლატფორმებს, რადგან დიდი ტესტირება დასჭირდება. მაგათ ალბათ ფრონტში მოუწევთ ვალიდატორების და კონვერტერების დაყენება, რომ მომხმარებლის მთავრული ასოები ძველ ჯავაში ან სხვაგან კარგად წავიდეს.

საერთო ჯამში, მე მომწონს რომ მთავრული დაამატეს (რამდენიმე ადამიანის დიდი შრომის შედეგად). ქართული ენის ნაწილია და არ უნდა დაიკარგოს.

თქვენ ხომ არ გაქვთ იდეები, კიდევ რაზე აისახება ეს ცვლილება?

ბმულები მთავრული ასოების შესახებ:
On.ge – UNICODE-მა ქართული მხედრული ანბანის 46 მთავრული ასონიშანი დაამტკიცა
DevFest 2016: Akaki Razmadze –  ❤  [I LOVE UNICODE]
DevFest 2017: Akaki Razmadze – გუტენბერგი, სტივ ჯობსი, გუგლი, ხინკალი, უნიკოდი
DevFest 2016: Michael Everson – The confusing case history of Georgian in Unicode

რატომ PHP?

threads

ეს მორიგი სტანდარტული პოსტი არ არის ენების / პლატფორმების დაპირისპირებაზე. უამრავი ნეგატიური სტატიის და მოსაზრების მიუხედავად წლებია რაც php-ის ჩემს გულში განსაკუთრებული ადგილი უჭირავს და სულ ვცდილობდი დამედგინა რატომ. ბოლოს სიყვარულის სიბრმავეს დავაბრალე და ტკბილ მოგონებებს, რაც საყვარელ ხალხთან და პროექტებთან მაკავშირებდა, მაგრამ ახლა მე მგონი კიდევ ვიპოვე ერთი დიდი მიზეზი.

მოდი ცოტა შორიდან დავიწყებ.
ერთხელაც, .net რომ ვითარდებოდა ვებში, შემოვიდა asp ვებ ფორმები და პოპულარული გახდა. რამდენჯერ ვცადე შიგნით გარკვევა, რა წიგნები არ წავიკითხე, მაგრამ არ მიმივიდა გული არცერთხელ, თუმცა მიზეზს ვერ ვასახელებდი მაშინ. ეგ მიზეზი იყო თავსმოხვეული ხელოვნურობა. გეუბნებოდა ფრეიმვორკი, რომ მე ვიცი რასაც ვაკეთებ, შენ მარტო ეს ღილაკი დასვი და იმას დააჭირე. მერე მიდიოდა და ზურგს უკან რას აკეთებდა?! 😀 ამხელა სტეიტს დაათრევდა აქეთ იქით რიქვესთებს შორის. მე მგონი პლატფორმა კი არა, ერთი დიდი workaround იყო ვებისთვის.

ვერ ვიტან როცა ფარდის უკან აკეთებენ ყველაფერს. მოხდება შეცდომა და კაცმა არ იცის ვისი და რისი გამოწვეულია სინამდვილეში.

ახლა ჯავა 😀 ჯავა მიყვარს. მაგრამ ვებში ვერ შევეგუე ჯავას. მე მგონი რეალურად ახლა გავიაზრე თუ რატომ ვიყენებთ მისთვის Application სერვერს და არა ვებ სერვერს. უხეში ანალოგია რომ გავაკეთო, წარმოიდგინეთ რომ როგორც დესკტოპისთვის გაუშვებთ რაღაც პროგრამას, ანალოგიურად უშვებთ თქვენს ერთ პროგრამას სერვერზე ყველა მომხმარებლისთვის. ერთს! ყველასთვის! მთელი პარალელიზმი შიგნით არის ფაქტიურად.

Java EE-ში რიქვესთების საპასუხოდ პროგრამისტები ვწერთ სერვლეტ კლასებს, იქ კი instance ცვლადების გაკეთება არ არის კარგი. იცით რატომ? იმიტომ რომ თითო სერვლეტის ერთი ობიექტი გამოიყენება ყველა რიქვესთის საპასუხოდ (მთლად მაგაზე დაყრდნობაც არ შეიძლება, რადგან კონტეინერი აკონტროლებს როდის მოკლას ან ჩაანაცვლოს ის ინსტანსი). სტატიკური რაღაცები, სინგლტონ კლასები – ყველაფერი ჩვეულებრივ არსებობს აპლიკაციაში და shared არის მომხმარებლებს შორის. სპეციალურადაც კი არის ობიექტი – ‘აპლიკაციის კონტექსტი’ რომელიც სერვლეტებს შორის shared მონაცემებს დაატარებს და არაერთ best practice-ში გამოიყენება, თუნდაც რაღაც ობიექტების კეშირებისთვის. ამ ყველაფერს კი სჭირდება სინქრონიზაცია. უნდა იცოდეს პროგრამისტმა რა ბიბლიოთეკები და კლასებია thread-safe, იცოდეს კოდის რა ნაწილი მოხვდება ცალკე ნაკადში და რა ნაწილი იქნება გაზიარებული. მერე შესაბამისად lock-ებით და სინქრონიზაციის სხვადასხვა მექანიზმებით უნდა აკონტროლოს ეს ყველაფერი.

იყო რაღაც ფრეიმვორკები, ახლაც არის. ახლების ნახვის სურვილი აღარც გამჩენია მათ შემდეგ, იმიტომ რომ მე მგონი იმ ვებ ფორმებზე უარესად აფარებდნენ ფარდებს ყველაფერს. მახსოვს Struts რომ მიშრობდა სისხლს. xml-ში აკონფიგურირებდი და მერე ჯადოსნურად უნდა ემუშავა ბევრ რამეს. მაგრამ უმეტეს გაშვებაზე ერთი შეცდომა გამოდიოდა ხოლმე, რაც ჭირივით მეზიზღებოდა 😀 ფაილს ვერ ვპოულობო. ვერ გაიგებდი რა უნდოდა, xml-ში გამოგრჩა წერტილი თუ გადატვირთვა დააკლდა სერვერს. Spring-ზე არც დავიწყებ…

asp.net-ზე როცა იწერება ვებ აპლიკაცია, იქაც ერთი პროგრამა ეშვება. იქაც თუ შექმნით სტატიკურ კლასს, საერთო იქნება და რაღაც საერთო კონტექსტის ობიექტიც ჰქონდათ თუ სწორად მახსოვს. მერე asp MVC ფრეიმვორკი რომ შექმნეს, იქ კარგად ცდილობდნენ ყველა რიქვესთი stateless ყოფილიყო და როცა მომიწია მაგით წერა, ერთი სიამოვნება იყო ჩემთვის რადგან ფაქტიურად ზუსტად ისე ვწერდი როგორც php-ში.

(ცოტა ვურევ ახლა მე ერთმანეთში რაღაც რაღაცებს, მაგრამ ყველაფერზე რომ ვრცლად დავწერო, მთლიანი ბლოგი არ მეყოფა ალბათ).

განა ვაკნინებ ამ პლატფორმებს და ფრეიმვორკებს (ვებ ფორმების გარდა), უამრავი არაადამიანური შრომა არის ჩადებული და მეც წლების მანძილზე გამომიყენებია. მადლობის მეტი რა მეთქმის.
მარტო იმას ვამბობ რომ გაცილებით სასიამოვნო და კომფორტულია ისეთ გარემოში წერა, სადაც ამხელა კომპლექსურობა მოხსნილია და თანაც ისეა მოხსნილი რომ ფარდის უკან გადაძრომა არ გიწევს ხშირად. ჩემთვის ერთ-ერთი ეგეთი გარემოა php.

მეორე გახდა node.js, რომელზეც ქვევით ვიტყვი უფრო დეტალურად.

როცა PHP-ის ვებ სერვერზე ვიყენებთ, უმეტეს შემთხვევაში ისე ვაკონფიგურირებთ რომ ის არ ინახავს არანაირ state-ს. თუ CGI-ის, FastCGI-ის ან Apache-ს Prefork რეჟიმს გამოვიყენებთ, სერვერთან მოსული თითოეული რიქვესთისთვის ახალი პროცესი გაეშვება და მარტო ერთ ნაკადში შესრულდება php-ის კოდი. მაშინაც კი თუ php-ის კომპილირებული ფაილების კეშირებას ვაკეთებთ, სკრიპტის თითოეული ინსტანსი მაინც ცალკე შეიქმნება და ფიზიკური მეხსიერებაც ცალკე გამოეყოფა.
თურმე სახელიც ჰქონია ასეთ მიდგომას – Shared nothing architecture.

მესმის რომ წარმადობის პრობლემა შეექმნება ასე, მაგრამ საშუალო ზომის ვებ აპლიკაციების შემთხვევაში რამდენჯერ მისულხართ მაგ პრობლემამდე?

(ცალკე დეველოპმენტის რეჟიმში კიდევ – არ ვიცი როგორ ახერხებს რომ კოდი სერვერის მხარეს კომპილირდება და მაინც წამიერად მუშაობს. არადა ჯავაზე ან visual studio-ში თუ ვარ, ყველა გაშვებაზე უნდა დაბილდოს-დალინკოს და აშკარად ვამჩნევ მაგას. დაკომპილირებულ ბიბლიოთეკებში ბოდიალი სხვა თემაა კიდევ..)

ჰოდა node.js-ს რაც შეეხება, მანდ საერთოდ მარტო ერთი ნაკადი გვაქვს. მერე უკან, ჩვენს გარეშე კი აქვს რაღაცები გაპარალელებული, მაგრამ პროგრამისტები რასაც ვწერთ, იქ ერთისთვის ვწერთ და სრულიად არ გვანაღვლებს race condition-ები. მაგის მოდელზე ჯობია ცალკე პოსტს დავწერ.
node-ში წერასაც ექნება თავისი ნაკლები და ბევრჯერ წავიმტვრევ ალბათ ცხვირსაც, მაგრამ ნაკადებზე რომ არ ვფიქრობ, ძალიან მომწონს.

ისიც მესმის, რომ ნელ-ნელა გარდაუვალი გახდება წარმადობაზე ფიქრი და წინსვლისთვის მაინც მოგვიწევს ისეთი პროგრამების შექმნა, რომლებიც ბევრი პროცესორისთვის და ბევრი ნაკადისთვის იმუშავებს, მაგრამ რაღაცნაირად იმედი მაქვს რომ იქნება რაღაც იდეები (მაგალითად როგორც node.js ან როგორც nosql ბაზები) და ნაწილობრივ მაინც მოგვაცილებენ shared state-ის თავისტკივილს.

მეტყვით ალბათ, რომ სიზარმაცის და Thread-ების აზრზე არ ყოფნის გამოა ეს ყველაფერი გამოწვეული. მაგის უარყოფას არ დავიწყებ 🙂

მილიარდ დოლარიანი შეცდომა

ასეთი სახელი ჰქონდა ტონი ჰოარის პრეზენტაციას 2009 წლის კონფერენციაზე სადაც პროგრამირების ისტორიაში ცუდი გადაწყვეტილებების შესახებ იყო მოხსენებები. მე მხოლოდ შარშან ვნახე და მას შემდეგ კიდევ ბევრმა წყალმა ჩაიარა, მაგრამ პრინციპში დღესაც აქტუალურია ეს საკითხი და მე მგონი ცალსახად ეფექტური გამოსავალი ჯერ ჩამოყალიბებული არ არის.

პრეზენტაცია 1965 წელს გამოგონილი null reference-ს ეხებოდა, რომელიც მილიარდ დოლარიან შეცდომად შეაფასა მისმა შემქნელმა:

“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.”

რეალურად, მე არ ვფიქრობ რომ თვითონ null reference-ის შექმნა იყო არასწორი, რადგან მის გარეშე ალბათ ძალიან ძნელი იქნებოდა თუნდაც OOP პროგრამირება, გაცილებით ზედმეტი ლოგიკის წერა და უარესი კომპრომისების და ბაგების მოვლა მოგვიწევდა. ამიტომ შეგვიძლია უფრო type checking სისტემებს დავაბრალოთ null-თან დაკავშირებული პრობლემები. ყოველ შემთხვევაში, ჰოარის პრეზენტაციიდანაც დაახლოებით ასეთი აზრი გამოვიტანე.

კონკრეტულად რომ ვთქვათ აი ასეთ პრობლემაზეა საუბარი (ორაკლის საიტიდან ავიღებ მაგალითს):
წარმოვიდგინოთ ჩანაწერი..

String version = computer.getSoundcard().getUSB().getVersion();

როცა ჯავაში აღვწერთ მეთოდს, მას ვუთითებთ დასაბრუნებელ ტიპს. რეალურად დასაბრუნებელი მნიშვნელობა არ არის ამ ტიპის, არამედ ერთ-ერთია სიმრავლიდან { null, ეს ტიპი }

ფაქტიურად ნებისმიერი მეთოდისგან (რომელიც ობიექტს აბრუნებს) შეიძლება null reference მივიღოთ შედეგად და სანამ ამ შედეგს გამოვიყენებთ, შემოწმება გვიწევს.
ზევით აღწერილი კოდი უსაფრთხო რომ იყოს, ასე უნდა ჩაგვეწერა:

String version = "UNKNOWN";
if(computer != null){
    Soundcard soundcard = computer.getSoundcard();
    if(soundcard != null){
        USB usb = soundcard.getUSB();
        if(usb != null){
            version = usb.getVersion();
        }
    }
}

თუმცა, როგორც წესი, უფრო ხშირად ვცდილობთ რომ ასეთი ჩანაწერი თავიდან ავირიდოთ. best practice-ების მიხედვით რეკომენდირებულია მეთოდი ისე ვწეროთ რომ მან არასდროს დააბრუნოს null, არამედ, ან რაღაცა ცარიელი ობიექტი მიიღოს შედეგად ან checked შეცდომა ისროლოს. ასე ნაწერი კოდი გაცილებით გვაზღვევს runtime-ში null pointer შეცდომებისგან.

მხოლოდ რეკომენდაციების იმედზე რომ არ ვიყოთ, ჯავას მერვე ვერსიაში შემოვიდა Optional ტიპი და ასევე @NonNull @Nullable ანოტაციები და ეს რამე რუმეები საშუალებას გვაძლევს მეტად ინფორმატიული იყოს ჩვენი კოდი და ცოტა კომპილაციის დროსაც მოგვეხმაროს. თუ ვიცით რომ რომელიმე ობიექტი შეიძლება null გახდეს, შეგვიძლია ის Optional კლასის საშუალებით შევინახოთ. და მერე გამოყენების დროს გავითვალისწინოთ ეს საშიშროება.

ჯავაში ასე გამოვიდოდა getSoundcard მეთოდის სიგნატურა:

Optional<Soundcard> getSoundcard()

მერე გამოყენებისას უნდა დავწეროთ

Optional<Soundcard> optional = getSoundcard()
if (optional.isPresent()) {
    Soundcard soundcard = optional.get();
    …..
}

ან სხვა უფრო მოსახერხებელი მეთოდების გამოყენება შეიძლება – orElse, ifElse, ifPresent.. ა.შ.

ზოგ ენაში შემოიღეს მოკლე ჩანაწერიც, მაგალითად Swift-ში შეგხვდებოდათ ასეთი რამ:

var version = computer?.soundcard?.USB?.version

ამ ჯაჭვში ერთ ერთი მაინც თუ დააბრუნებს null-ს, მომდევნო getter-ების გამოძახება აღარ გაგრძელდება და მთელი შედეგი null გამოვა.

Groove ენაში არის ელვისის ოპერატორიც 😀 რომელიც ასე გამოიყურება ?:

String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";

და მაგითი default მნიშვნელობა შეგვიძლია შემოვიტანოთ null-ის შემთხვევაში.

ბოლო რამდენიმე თვეა რაც Swift-ზე წერას ვცდილობ. ჯერჯერობით ენა არ არის სრულად ჩამოყალიბებული და ხშირად იცვლება. თუმცა ეს კონცეპტი ალბათ არსად წასვლას არ აპირებს. თავიდან ძალიან გამიჭირდა იმასთან შეგუება, რომ ენა გაიძულებს ცვლადის გამოცხადებისთანავე განსაზღვრო optional იქნება ის თუ არა. თუ კი, მაშინ ყველგან სადაც ამ ცვლადის გამოყენებას შეეცდებით, მისი null-ზე შემოწმება და მნიშვნელობის ე.წ. unwrap მოგიწევთ optional ტიპის ცვლადისგან. კოდი არ დაკომპილირდება სხვა შემთხვევაში.

ამ პოსტში ელდარი უნდა დავთაგო : )) ერთხელ მასთან ვიწუწუნე ამ თემაზე და იმ დღეს დამაფიქრა, შემაცვლევინა აზრი. მერე წავედი და გემრიელად დავარეფაქტორე კოდი. არც ისეთი ცუდი ყოფილა ეს optional ტიპი. რაც დრო გადის, ვხვდები, რომ კარგ საქმეს მიკეთებს და მაიძულებს არ გავახალტურო რაღაცები. ასე რომ ვნახოთ.. იქნებ ჯავაშიც გაგვილამაზონ სინტაქსი თორემ დაიკარგება კაცი ამდენ boilerplate კოდში.

მრავალგანზომილებიანი მასივი Java-ში

გუშინ ერთ-ერთ ამოცანაში მეხსიერების ლიმიტს ვაჭარბებდი იმის გამო, რომ არასწორად მქონდა აღწერილი მრავალგანზომილებიანი მასივი. c-ში და c++-ში ამის პრობლემა არ იქნებოდა, თუმცა, იმის გამო, რომ ჯავაში ყველაფერი ობიექტია (პრიმიტიული ტიპების გარდა), მრავალგანზომილებიანი მასივების შექმნისას მეხსიერება განსხვავებული გზით უნდა დავითვალოთ.

მაგალითად ასეთი მასივის ზომა c++-ში 96 ბაიტია (32 ბიტიან სისტემაში):
int arr[2][3][4];
2 * 3 * 4 = 24. თითოეული კიდევ 4 ბაიტიანია int ტიპის გამო და ჯამში 24 * 4 = 96 ბაიტი გამოდის.
int arr[4][3][2] ასეთი აღწერის დროს შედეგი იგივეა.

შევადაროთ Java-ში ეს ორი აღწერა:
int arr[][][] = new int[40000][200][2];
int arr[][][] = new int[2][200][40000];

აზრობრივად განსხვავება არ არის, თუმცა პირველი გზა გაცილებით მეტ მეხსიერებას მოიხმარს. აი, რატომ:
1. ჯავას მასივი თავისთავად ობიექტია. თითოეულ ობიექტში (არა reference-თან, არამედ heap-ში) დამატებით რამდენიმე ბაიტს იკავებს სპეციალური object header ინფორმაცია. object header-ში ინახება ვირტუალური მანქანისთვის საჭირო მონაცემები, რასაც garbage collector-ისთვის და სხვადასხვა მიზნებისთვის იყენებს. როგორც ვიცი, ჩვეულებრივ ეს 8 ბაიტია ხოლმე 32-ბიტიან მანქანაზე და 16 ბაიტი 64-იანზე. ამის გარდა, მასივის ობიექტში ინახება მასივის ზომაც, ანუ დამატებით 4 ბაიტი. კიდევ შეიძება მეხსიერებაში padding-ს დასჭირდეს რამდენიმე ბაიტი. ამიტომ ზუსტად არ დავთვლი და ვთქვათ, ერთი მასივის ობიექტისთვის (ელემენტების გარდა) საჭიროა X ბაიტი.

2. int[a][b] – ჯავაში ეს არის a ცალი b ელემენტიანი მასივის მასივი, ანუ სინამდვილეში ერთის მაგივრად a+1 ობიექტი გვაქვს.
int[a][b][k]სამგანზომილებიანის შემთხვევაში a * b + a + 1 ცალი ობიექტი და ა.შ.

ახლა გამოვთვალოთ და შევადაროთ ზევით ნახსენები ორი მასივის ზომა:
(40,000 * 200 + 40,000 + 1) * X + (40,000 * 200 * 2 * 4)
(2 * 200 + 2 + 1) * X + (40,000 * 200 * 2 * 4)

მეორე ნაწილი რაც int ტიპის ელემენტების ზომას ითვლის, ცხადია, ორივესთვის ერთი და იგივე იქნება.
პირველი ნაწილის მიხედვით კი 8,039,598 ცალი ზედმეტი ობიექტი იქმნება პირველ შემთხვევაში, მეხსიერებაც შესაბამისად გაცილებით მეტი სჭირდება.

ისე, პროფაილერით ვერ ვხედავ სინამდვილეში მართლაც ამდენი ობიექტია თუ არა და იდეა ხომ არ გაქვთ როგორ შეიძლება შევამოწმო?

დინამიური მასივი

პროგრამირებაში დინამიური მასივი ისეთი მონაცემთა სტრუქტურაა, რომელსაც ცვლადი სიგრძე აქვს, მასში ელემენტების ჩამატება / წაშლა და random access შეიძლება – ანუ მის ნებისმიერ ელემენტზე წვდომას ფიქსირებული დრო სჭირდება და მასივის მთლიან ზომაზე არ არის დამოკიდებული.

ასეთ სტრუქტურას სხვადასხვა დასახელებით შეხვდებით – dynamic array, growable array, resizable array, dynamic table, mutable array, array list.

random access-ის უზრუნველსაყოფად მასივისთვის მეხსიერების უწყვეტი ნაწილი გამოიყოფა, ანუ, მისი ელემენტები მეხსიერებაში ერთმანეთის მიყოლებით ინახება.
სტატიკური მასივის შემთხვევაში ეს პრობლემას არ წარმოადგენს, რადგან ჩვენ თავიდანვე ვეუბნებით სიგრძეს. დინამიურ მასივს კი სწორედ მაშინ ვიყენებთ, როდესაც წინასწარ არ შეგვიძლია ზომის განსაზღვრა. მაშინ რამხელა მეხსიერება უნდა გამოიყოს, რომ ელემენტები კვლავ მიყოლებით არსებულ ბაიტებში იქნას შენახული?

ცხადია, თავის დაზღვევის მიზნით დიდი მეხსიერების გამოყოფას აზრი არ აქვს. 20 ელემენტის შენახვისთვის მილიონი ბაიტი წინასწარ არ უნდა დარეზერვდეს ასეთი მასივისთვის, რომელიც შემდეგ ცარიელი დარჩება.

ბევრ პროგრამირების ენაში ეს პრობლემა შემდეგნაირად არის გადაწყვეტილი:
დინამიური მასივის უკან სტატიკური მასივი დგას, რომელიც დასაწყისში მცირე სიგრძის მითითებით იქმნება. როდესაც მასივი გაივსება და შემდეგ კვლავ შეეცდებიან მასში ელემენტის ჩამატებას, შეიქმნება უფრო დიდი ზომის სტატიკური მასივი, მასში გადაიწერება არსებული მასივის ელემენტები და ძველი წაიშლება. შესაბამისად, დინამიურ მასივში ზოგიერთი ელემენტის ჩამატებას შედარებით მეტი დრო მიაქვს.

ასეთ გადაწყვეტაში მნიშვნელოვანია შეკითხვაზე პასუხი: რამდენჯერ უნდა გაიზარდოს მასივი გადაწერის წინ?

აღვნიშნოთ, რომ ისევ კომპრომისს ვეძებთ დროის და მეხსიერების ხარჯვას შორის. თუ თითო-თითოდ გავზრდით მასივს, ყოველ ახალ ელემენტზე მთელი მასივის გადაწერა დროს წაიღებს, ხოლო თუ რამდენჯერმე დავაგრძელებთ, შეიძლება დიდი ზომის მეხსიერება ცარიელი დაგვრჩეს.

ოპტიმალურ ვარიანტად რიცხვი 2 არის მიღებული. ამ რიცხვს ზრდიან ამ ამცირებენ იმის მიხედვით მეტი მეხსიერების ხარჯი ურჩევნიათ თუ დროის.

სხვადასხვა პროგრამირების ენაში ან რეალიზაციაში ის ზოგჯერ განსხვავებულია – მაგალითად, ვექტორი c++-ში 2-ჯერ იზრდება, Java-ს ვექტორისთვისაც default მნიშვნელობა ეს რიცხვია, თუმცა პარამეტრის სახით არის და შეცვლა შეიძლება. Java-ს ArrayList-ის სტატიკური მასივი 3/2-ჯერ იზრდება. HashMap-ის მასივი 2-ჯერ. პითონის C-ის იმპლემენტაციაში რაღაც უცნაურად არის, 9/8 გამოდის საშუალოდ. ამ ბმულზეა კოდი. აქ კი მისი გარჩევა

თუ პროგრამისტისთვის წინასწარ არის ცნობილი მინიმუმ რამხელა მასივი დასჭირდება, შეუძლია ეს გაითვალისწინოს და დინამიური მასივი თავიდანვე იმხელა შეიქმნას.
მაგალითად c++-ის vector-ს აქვს ფუნქცია reserve რომლითაც წინასწარ შეიძლება მეხსიერების რეზერვირება.

Java-ში ArrayList და HashMap ობიექტებს კონსტრუქტორში გადაეცემა initialCapacity პარამეტრი. HashMap-ში არა მხოლოდ მასივის გადაწერა, არამედ ჰეშების თავიდან გენერაცია ხდება.

წარმადობისთვის ამ პარამეტრის წინასწარ მითითება უმჯობესია. რამდენიმე ექსპერიმენტი ჩავატარე და მართლაც მივიღე დროში სხვაობა, თუმცა ჩემს ჩვეულებრივ ამოცანებში ასეთი სხვაობა უმნიშვნელოა. ბოლო ბოლო ლოგარითმული რაოდენობით ხდება მასივის გადაწერა და მილიონის შემთხვევაშიც კი ეს სულ რაღაც 20-ია. თუ მილიწამები მნიშვნელოვანია, იქ ღირებული იქნება ასეთი ოპტიმიზაცია.

ზევით ვახსენეთ, რომ დინამიურ მასივში ამ გადაწერის საჭიროების შედეგად ზოგიერთი ელემენტის ჩამატების დროს O(1)-დან იზრდება O(n)-მდე, სადაც n მასივში არსებული ელემენტების რაოდენობაა. ამის მიუხედავად, ამორტიზებული ანალიზის შედეგად, დინამიურ მასივში ელემენტის ჩამატების დრო O(1)-ად არის მიჩნეული.

თვითონ ამორტიზებული ანალიზის არსი იმაში მდგომარეობს, რომ ალგორითმის მუშაობის შეფასებისას გათვალისწინებული იქნას ის სწრაფი ოპერაციებიც, რომელიც ყველაზე ცუდი შემთხვევების მუშაობის დროს აბათილებენ. ალგორითმის შესრულების დროის შეფასებისას worst-case სცენარს ვიღებთ ხოლმე, თუმცა შეიძლება რომ დასამუშავებელ ინფორმაციაში მხოლოდ რაღაც პროპორციით იყოს მძიმე ოპერაციები. შესაბამისად, თუ ჩვენ ყველა ოპერაციას გავითვალისწინებთ შეფასების დროს, შესაძლოა მუშაობის დრო გაცილებით ნაკლები გამოვიდეს, ვიდრე რაოდენობა გამრავლებული ყველაზე დიდ დროზე.

მოდი გამოვთვალოთ n ელემენტიანი დინამიური მასივის შევსების დრო:
თუ ჩავთვლით რომ ყოველ ჯერზე (როდესაც ორის ხარისხს მივაღწევთ) სტატიკური მასივი ორმაგდება, შეგვიძლია ელემენტების გადაწერის რაოდენობა ასე შევაფასოთ:
ბოლოდან მოვყვეთ. ბოლოს ყველას გადაწერა მოუწევს, იმის წინ მხოლოდ ნახევრის, იმის წინ მეოთხედის და ა.შ.
n + n/2 + n/4 + n/8 + … = n (1 + 1/2 + 1/4 + 1/8 + …) = 2n

ამ გადაწერებს მივუმატოთ თვითონ ელემენტების ჩასმაც და გამოვა 3n. შესაბამისად საშუალოს თუ ავიღებთ, თითოეული ელემენტის ჩასმისთვის გამოგვივა O(3) = O(1) დრო.