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

ასეთი სახელი ჰქონდა ტონი ჰოარის პრეზენტაციას 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 კოდში.

საერთო კოდი ბრაუზერსა და node.js აპლკაციაში

წინა პოსტებში ვახსენე რომ ჯავასკრიპტზე მომიწია ანიმაციის ლოგიკის წერა. იგივე კოდი (მათ შორის გეომეტრიაც) ბექშიც მჭირდებოდა, ამიტომ ვიფიქრე – რატომ გადავწერო.. ბექს node.js-ით გავაკეთებ და იგივე კოდს გამოვიყენებ. შესაბამისად თავსებადობის ამოცანა გამიჩნდა.

ცხადია, პირველ რიგში საერთო ბიბლიოთეკებიდან უნდა გამომეყო ბრაუზერთან დაკავშირებული კოდი (DOM-ის მანიპულაცია, ვორკერების ურთიერთობა, ა.შ.). მეორე რიგში node.js-ს ცოტა განსხვავებული მეთოდი აქვს სკრიპტების იმპორტისთვის. თუ ბრაუზერში <script> ტეგს ვიყენებთ, node.js-ში მოდულები უნდა გავაკეთოთ და require მეთოდით ჩავტვირთოთ.

მაგალითად, მარტივი მოდული ასე გამოიყურება (ფაილის სახელია Square.js):

exports.Square = function (side){
	this.side = side;
	this.getArea = function(){
		return side * side;
	}
}

module.exports ან პირდაპირ exports მეთოდს ვიყენებთ იმისთვის რომ ობიექტები გარედან ხელმისაწვდომი გავხადოთ. ამ ფაილში აღწერილი სხვა დანარჩენი ფუნქციები და ცვლადები private იქნება გარე სამყაროსთვის.

მოდულის გამოყენების მაგალითი სხვა ფაილში:

var module = require('./Square');
var square = new module.Square(10);
console.log(square.getArea());

Square.js ფაილი ბრაუზერისთვის თავსებადი რომ გავხადოთ, exports მეთოდს რამე უნდა მოვუხერხოთ.
მაგალითად, შეიძლება შემოწმების ჩამატება:

var Square = function (side){
	this.side = side;
	this.getArea = function(){
		return side * side;
	}
}

if (typeof exports !== 'undefined') {
	exports.Square = Square;
}

აქ კიდევ ერთი მომენტია. როგორც ზევით აღვნიშნეთ, node.js-ში exports-ის გარეთ რაც რჩება, ხელმისაწვდომი არ არის გარედან. ბრაუზერის შემთხვევაში კი Square გლობალური ცვლადი გამოდის. სწორი იქნებოდა, რომ მისი წვდომაც შეგვეზღუდა. მაგალითად ასე:

(function(exports){
	var Square = function (side){
		this.side = side;
		this.getArea = function(){
			return side * side;
		}
	}

	if (typeof exports !== 'undefined') {
		exports.Square = Square;
	}	
})(typeof exports !== 'undefined' ? exports : this['module'] = {});

 

RequireJS + node

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

ზევით აღწერილი მოდულის მაგალითი Require.js-ით:

define(function(){
	return function Square(side){
		this.side = side;
		this.getArea = function(){
			return side * side;
		}
	}
});

მოდულის გამოყენების მაგალითი:

requirejs(['Square'], function(Square) {
    var square = new Square(10);
	console.log(square.getArea());
});

JavaScript animation / game loop (ნაწილი 2)

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

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

აქედან გამომდინარე, უმჯობესია რომ ანიმაციის ან თამაშის მდგომარეობები ცალკე გამოითვალოს / დაგენერირდეს და requestAnimationFrame-ის ქოლბექში მხოლოდ მათი რენდერის კოდი ჩაიწეროს.

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

1. შეიძლება setInterval და setTimeout მეთოდებით დაგენერირდეს მდგომარეობები, ხოლო requestAnimationFrame-ის ქოლბექმა აარჩიოს იქიდან შესაბამისი მდგომარეობა და ის დაარენდეროს.

2. პირველი პუნქტის ანალოგიურად მდგომარეობები დროის რაღაც ინტერვალში დაგენერირდეს, თუმცა ეს ლოგიკა ცალკე ნაკადში – მუდმივად გაშვებულ web worker-ში იყოს გატანილი, რომ მთავარი ნაკადი ძალიან არ დაიტვირთოს (ჩემი აზრით ეს უკეთესია). requestAnimationFrame-მა კი worker-დან მიღებული მდგომარეობებიდან შეარჩიოს დროის შესაბამისად და დაარენდეროს გარემო.

3. ეს მეორე პუნქტის მსგავსია, თუმცა setInterval-ის ნაცვლად ინიციატორი requestAnimationFrame-ის ქოლბექი იყოს. მის გამოძახებაში დამატებით იყოს worker-თან შეტყობინების გაგზავნა, რომ ვორკერმა მომდევნო X რაოდენობის მდგომარეობა დააგენერიროს. ასეთ შემთხვევაში, თუ ბრაუზერი არ არენდერებს არაფერს, ჩვენი ნაკადი ტყუილად არ იქნება მუდმივად გაშვებული თავისი მძიმე კალკულაციებით.

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

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

JavaScript animation / game loop (ნაწილი 1)

იმ დღეს ერთ-ერთ ვებ აპლიკაციაში არც თუ ისე მარტივი ანიმაციის გაკეთება მომინდა და ბოლოს ჯავასკრიპტამდე მივედი. თუმცა რამდენიმე გზის გამოკვლევა დამჭირდა და გადავწყვიტე ამ თემაზე პოსტი კარგი იქნებოდა. იქნებ ვინმეს უკეთესი იდეა ჰქონდეს…

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

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

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

var animatedElement = document.getElementById("animatedElement");
var transform = window.getComputedStyle(animatedElement).webkitTransform;
var curTransform = new WebKitCSSMatrix(transform);
console.log(animatedElement.offsetLeft + curTransform.m41);
console.log(animatedElement.offsetTop + curTransform.m42);

ამიტომ გადავწყვიტე ჩემს შემთხვევაში მხოლოდ CSS3-ის იმედზე ვერ ვიქნებოდი და ისევ ჯავასკრიპტს მოვკიდე ხელი. იდეაში საჭიროა შემდეგი: უნდა გავაკეთო კადრების ციკლი. დროის პატარა ინტერვალებში უნდა მოხდეს ელემენტების მდგომარეობის / მდებარეობის განახლება და შესაბამისად გადახატვა. კოლიზიების შემოწმებაც მანდ შეიძლება – მდგომარეობის გენერაციის დროს.

წინათ ჯავასკრიპტის ანიმაცია რომ გაგეკეთებინათ, ერთმანეთში ჩადგმული და გადაბმული setTimeout ან setInteval მეთოდებით მოგიწევდათ ანიმაციის ციკლის შეკვრა. მაგალითად, ასე:

function draw(){
    console.log('timestamp ' + +new Date());
    // update positions and redraw frame
}
setInterval(draw, 10);

ყოველ 10 მილიწამში მოხდება draw მეთოდის გამოძახება და მაგ მეთოდში იქნებოდა ანიმაციის ახალი მდგომარეობის გენერაცია.

ზოგი ახლაც იყენებს ამ გზას მარტივი ამოცანების გადასაჭრელად, თუმცა გასათვალისწინებელია მათი პრობლემები:

1. არაზუსტი დრო. დაყოვნება, რომელსაც ამ მეთოდებს (setTimeout ან setInteval) მილიწამების სახით გადავცემთ არგუმენტად, მხოლოდ იმას ნიშნავს, რომ ჩვენი ფუნქციები ამ დროის შემდეგ ბრაუზერის UI thread queue-ში ჩაემატებიან და თავის რიგს დაელოდებიან შესასრულებლად. შესაბამისად თუ UI ნაკადი სხვა საქმითაა დაკავებული, ჩვენი ანიმაციის ფრაგმენტი ზუსტად საჭირო დროს ვერ შესრულდება.

2. Browser timer resolution. მისი ქართული შესატყვისი ვერ მოვიფიქრე – ეს არის დრო, რამდენ ხანში ერთხელაც ახლდება საათი. მაგალითად, წინათ ბრაუზერები OS-ის სისტემურ დროს უყურებდნენ, რომელსაც ეს რეზოლუცია გაცილებით დიდი ჰქონდა (მაგალითად 10-15 მილიწამი), შესაბამისად რამდენიმე მილიწამის მითითების შემთხვევაში, შედეგის განსაზღვრა ვერ ხდებოდა. დღეს მთლად ასე აღარ არის, მაგრამ ამაზე ლაპარაკი შორს წამიყვანს.

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

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

მაგალითად:

function step(timestamp) {
    console.log(timestamp);
    requestAnimationFrame(step);
}
requestAnimationFrame(step);

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

ეს მაღალი სიზუსტის ტაიმსტემპი არის DOMHighResTimeStamp ობიექტი და განსხვავდება ჩვეულებრივი Date.now() ფუნქციის დასაბრუნებელი მნიშვნელობისგან. თუ მაგ ფორმატის მიმდინარე ტაიმსტემპი გჭირდებათ, მისი მიღება შეიძლება performance.now() მეთოდით.

CNC მანქანების დაპროგრამება

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

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

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

lcamtuf_robot1

ყალიბის გაკეთება რობოტის ნაწილებისთვის. ფოტო აღებულია lcamtuf-ის საიტიდან.

othermill

Othermill კომპანია otherfab-ისგან