Register to get access to free programming courses with interactive exercises

Invariants JS: Building abstractions with data

Abstraction allows us to avoid thinking about the implementation details and focus on usage. Moreover, if necessary, you can always rewrite the abstraction implementation without stressing about breaking the code that uses it. But there's another important reason to use abstraction, finding invariants.

An invariant in programming is a logical expression that defines the consistency of a state or data set.

Let's look at an example. When we supplied the constructor and selectors for rational numbers, we implicitly had the following invariants:

const num = makeRational(numer, denom);
numer === getNumer(num); // true
denom === getDenom(num); // true

// Or considering normalization
// numer / denom === getNumer(num) / getDenom(num);

By passing the numerator and denominator to the rational number constructor, we expect to get the same numbers if we apply the selectors to that rational number. This is how we determine if the abstraction is done correctly. This code is practically a test!

Invariants exist for any operation. And sometimes they're pretty tricky. For example, rational numbers can be compared to each other but not directly because the same fractions can be represented in different ways: 1/2 and 2/4. Code that doesn't take this fact into account won't work correctly:

const num1 = makeRational(2, 4);
const num2 = makeRational(8, 16);

console.log(num1 === num2); // false

The action of reducing a fraction is called normalization. This includes several operations, such as shortening a fraction, determining the sign, and moving the sign to the numerator. Normalization can be done in different ways. The most obvious one is to execute it during fraction creation, inside the makeRational() function. The other is to perform normalization when accessing the fraction using getNumer() and getDenom(). The latter method has the disadvantage that normalization happens with every call. This can be avoided by using memoization.

Given the new inputs, it's clear that the invariant linking the constructor and selectors need to be modified. The getNumer() and getDenom() functions should return the normalized values and not the raw ones.

const num = makeRational(10, 20);
getNumer(num); // 1
getDenom(num); // 2

The abstraction not only hides the implementation from us, but it preserves the invariants. Any code without abstraction is fraught with the risk of internal transformations that can remain unnoticed:

// Traversing the constructor

// This data is not normalized because the constructor was not used
const num = { numer: 10, denom: 20 };

// Not what it is supposed to return (normalized return is expected):
getNumer(num); // 10
getDenom(num); // 20

// Direct modification

const num = makeRational(10, 20);
// there can be no normalization here, since it's a direct change
num.numer = 40 

getNumer(num); // 40
getDenom(num); // 20

I.e., working with data directly and avoiding abstraction can easily break invariants provided by additional logic in the constructor or selectors. So it's important to use the code the way the authors intended.

Looking at the examples above, you may have one genuine question. Is it possible to forbid direct access to data? Generally, yes. This approach is called data hiding. Usually, languages use a special syntax to hide data. However, data hiding can be done without using special tools: it requires higher-order functions. This method implies building abstractions with anonymous functions, closures, and message passing (more in SICP). <!---If you want to learn more about this, try our JS: Compound data course.--->


Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

About Hexlet learning process

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.bookmate">Bookmate</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.healthsamurai">Healthsamurai</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.dualboot">Dualboot</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.abbyy">Abbyy</span>
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.