Clicky

Computable Multiverse

Hi, I'm Rakhim. I teach, program, make podcasts, comics and videos on computer science at Codexpanse.com. You can learn more about my work and even support me via Patreon.


Coding vs. Programming vs. Software Engineering

I don’t think there are formal definitions for either “coding” or “programming” or even “software engineering”, even though the latter is used in formal contexts like academia and human resources. They are vague, pliable terms which mean different things to different people. I want to share how I sometimes think of them.

Programming

Programming is solving explicit problems in a verifiable manner.

It is similar to mathematical proofs. “Find an element in a sorted sequence” is a problem. A binary search algorithm is a solution. You can prove its validity. You can even try to prove or disprove whether it is the best possible solution in terms of efficiency.

A better word for programming is computing, which isn’t a new area at all, but rather a part of mathematics. For programming (computing), code is not relevant, algorithms are. Only essential complexity exists on this level, and no accidental complexity is a concern.

The goal of programming is to create an algorithm (or show it can’t be done).

Coding

Coding is expressing a programming solution in a formal language.

It is similar to typing or speaking. Binary search from the previous paragraph can be coded in C or JavaScript imperatively. Or drastically different in some declarative language. As long as the underlying algorithm is the same, different implementations are just different manifestations of the same idea.

Coding is simultaneously easier and harder than programming. It is easier because coding assumes the problem is already solved. It is harder because to encode a mathematical algorithm into a real-world system is to solve a non-explicit problem in a hardly verifiable manner (e.g. “make the app run on all devices”).

In a perfect world, coding would be strictly easier than programming. In our world, a lot of accidental complexity comes from coding.

The goal of coding is to write valid code.

Software engineering

Software engineering is building a product for the real world.

It is similar to civil engineering. It encompasses both programming and coding, as well as other areas, such as:

  • communication
  • management
  • compliance
  • durability
  • scalability

Depending on historical, cultural or political context, software engineering might expand to include more responsibilities. The majority of accidental complexity comes from SE.

The goal of software engineering is to solve real world problems.

Bang, Marry, Kill

I’d say for many devs, the following is true:

  1. Love programming.
  2. Enjoy coding.
  3. Tolerate software engineering.

Since a regular, normal career in tech is a combination of the three fields, many people come back even after severe burnouts. We love programming. Burnouts originate from engineering and maybe coding, but rarely programming. We all dream of the world where we create algorithms in the perfect, mathematical universe, then implement them in a perfect language for a consistent, well designed system. But reality requires us to deal with inconsistent formats, ambiguous requirements, needless complexity, infrastructure, deployment, delivery, relationships, communication, politics, finance, etc.

Why differentiate

These definitions aren’t helpful in everyday life. For the most part, we all use these words interchangeably, and we usually encompass everything under “programming”.

I find it valuable to differentiate between the three in times of reflection. When I feel energized and motivated, I want to know what drives me. I want to clearly see the position I’m in. When I feel depressed or burned out, I want to know the causes.

The differentiation is also helpful when you ask yourself: what area do you enjoy the most? There are no wrong answers: programming isn’t sacred, software engineering isn’t dirty, coding isn’t an afterthought.

November 27, 2019 | permalink

Examples are the best documentation

When I’m searching for docs, 95% of the time a single example would suffice. Yet, 95% of the time I can’t find one in any official source.

It seems that by default formal technical documentation is targeted towards someone who’s deeply immersed in the ecosystem. But many developers have to juggle a lot of “worlds” in their heads daily. When jumping between projects, languages and frameworks, it takes a considerable amount of mental energy to restore the context and understand what is going on.

Consider this example from the Python 3 docs:

max(iterable, *[, key, default]) Return the largest item in an iterable or the largest of two or more arguments… [followed by 5 short paragraphs].

You need to know quite a bit about Python in order to understand this:

  • What * means in the function definition.
  • What’s iterable.
  • What are keyword-only arguments.
  • What key usually means.

Then you have to read some text in order to understand what values you can pass and how to actually call the function.

Granted, these are important details that can’t be omitted for brevity. But I bet a lot of developers looked at that article simply because they needed to quickly find out how to pass a custom sorting function. This example would’ve quickly helped them:

max(4, 6) # → 6

max([1, 2, 3]) # → 3

max(['x', 'y', 'abc'],  key=len) # → 'abc'

max([]) # ValueError: max() arg is an empty sequence
max([], default=5) # → 5

Easy, right?

One popular community-based project in the Clojure world is clojuredocs.org, a site where people contribute examples for built in functions. It’s fantastic and, in my experience, indispensable in day-to-day coding. For example, check out the pages about into or spit or map. Note that examples often include related functions, not only those in question. This increases the real-world usefulness and practicality.

Since even major software projects rarely offer 4 distinct kinds of documentation, I am often hesitant to click on a “Documentation” link. Chances are, it’s a terse, difficult to read, automatically generated API reference. I often choose to find a tutorial, not because I need a walk-through, but because there are examples in it.

November 26, 2019 | permalink

Quick and dirty git push

Very often, all I need from git is to stage all changes, commit them and push to master. I’ve made two scripts for this. One in bash:

gitapush() {
  if [ $# -eq 0 ];
  then
    echo "Enter message: "
    read message
  else
    message=$1
  fi

  if [ -z "$message" ];
  then
    message=$(date +%d-%b-%H:%M)
  fi

  git add .
  git commit -m "$message"
  git push origin master
}

gitapush stands for “git all push” (I guess), and it’s pretty dumb:

  1. If argument is provided, use it as message, commit and push (e.g. gitapush fix x)
  2. If argument isn’t provided, as for a message.
    1. If a message was entered, commit and push.
    2. If not, generate a message from current date, commit and push.

To achieve a similar thing in Emacs, I use magit:

(defun my-magit-stage-all-and-commit-and-push () (interactive)
       (let ((m (read-string "Commit message: ")))
         (unless (string= "" m)
           (magit-stage-modified)
           (magit-call-git "commit" "-m" m)
           (magit-call-git "push" "origin" "master"))))

(global-set-key (kbd "s-G") 'my-magit-stage-all-and-commit-and-push)

This one doesn’t generate an automatic commit message, though. I bound it to Cmd+Shift+G, because Cmd+G is how I open the magit status pane.

November 22, 2019 | permalink

Pocket App fails silently

Pocket is a popular “read later” app. People generally recommend it, and I haven’t heard many complains about it. But in my experience, Pocket fails, and does it in the worst possible way.

Pocket repeatedly omits portions of pages. Here are a few examples from my queue — try adding them to your Pocket to verify the problem:

  1. This link: 11 images omitted.
  2. This link: The last paragraph and all images omitted.
  3. This link: First 50% of the article omitted.

Of course, parsing pages and extracting information isn’t an easy task, and occasional hiccups are to be expected. The only reason I’m bringing this up is that, at least in my case, a significant amount of links end up problematic. Instapaper, a popular alternative to Pocket, doesn’t have these issues on such scale.

The worst part of this is that you might not even notice something is missing, but end up thinking the article had a weird start (you missed the intro paragraph), or ends abruptly, or could use some images. Pocket delivers a poorly retold story.

Pocket also fails (non silently, that is) to process seemingly simple pages. For example, it couldn’t pocket-ify this page about Structured Procrastination. Yes, there are HTML validation errors, but both Instapaper and Safari’s Reader mode render the simplified version perfectly fine.

This isn’t new: I’ve been coming back to Pocket roughly every year in the past 10 years, and it’s always like that. It’s owned my Mozilla now, and even has a paid plan, yet Pocket fails at its main task.

I recommend to avoid Pocket.

November 21, 2019 | permalink

The price of complexity

Computer programmers often talk about tackling complexity, yet they thrive on complexity. I believe tech people experience a constant dilemma: on one hand, we want things to be simple and straightforward; on the other hand we love complex structures and engineering marvels.

I think about this today as I’m performing some cleanup work on my blog. It runs on Hugo, content is written in Org mode, code is published on Github and the final website is deployed to Netlify. That’s a lot of moving parts, and, honestly, it feels excessive. Yet I love this setup.

Lately I’ve been trying to be conscious and mindful about the price my mind pays for all this complexity. I’m not a good programmer by any means, so your mileage may vary, but it takes an enormous amount of mental energy for me to re-understand something I already figured out before. Take Hugo for example, a flexible and powerful static website generator. Jekyll, which I used before Hugo, is complex, too, but Hugo drives me crazy sometimes. It’s a multi-layered system of interconnected logic and it took me a whole day to move from Jekyll.

Intermission: I just spent 5 minutes trying to create a relative hyperlink to that other blog post, and couldn’t. It took me a while to realize I’m in Org mode now, not in Markdown, my syntax was wrong. You know what a Wordpress or Ghost user would’ve done? Clicked a link button in their rich WYSIWYG editor and had finished the blog post by this time already.

Every time I need to make some changes to the setup — fix layouts, add pages, refactor templates — I feel completely lost. It happens rare enough for my brain to forget the structure and conventions. And this is the case for a dozen of software projects I touch throughout the year.

This feeling of being lost is similar to un-pausing a video game that was on pause for 6 months. I know I’ve been into this, but right now I don’t even know what buttons to press.

There are ways to fight this. One is to dramatically reduce complexity in the first place, maybe even sacrifice some of the features. My setup can be technically replaced by a bunch of HTML files, for example. Or switch to a “normal” thing like Ghost or Wordpress. Oh, and don’t host them yourself, but pay someone to take care of it.

Another way is to somehow capture the knowledge for easy retrieval. My problem with Hugo is that I rarely touch it, so I forget. I should at least add a README file for myself, explaining the current setup and structure. Keeping documentation in sync with code is another problem, sigh…

So far, I only know one good way of solving this: teach. I should just make a course about Hugo on Codexpanse, that’ll force me to really understand it and devise a good mental model.

November 20, 2019 | permalink

User Is Dead

User is dead. User remains dead. And we have killed him. How shall we comfort ourselves, the developers, the designers, the growth hackers? What was holiest and the final judge of all that the world has yet owned has bled to death under our a/b-tests and new features. Who will wipe this blood off us? What garbage collector is there for us to clean ourselves? What conference of atonement, what disruptive technology, what sacred meeting shall we have to invent? Is not the greatness of this deed too great for us? Must we ourselves not become users simply to appear worthy of it?

By saying “God is dead” Friedrich Nietzsche tried to express the fear that the decline of religion and the rise of nihilism would plunge the world into chaos.

Whether you believe that people require an external source of morality, or you accept the numerous philosophical and scientific arguments for intrinsic morality in behavior of complex animals, the fear remains relevant. The fear is not necessarily about God or religion, but about a moral compass, or lack thereof.

My arguably tasteless rewrite of Nietzsche’s passage expresses another fear, which, I sincerely hope many software developers share. For a long time, we had an external source of morality in software development. User. Simultaneously ephemeral stick figure of UML diagrams and a very real human, a friend, a colleague. The goal, the point, the final judge of success. Your product either satisfied the user or failed. Your software may occasionally have charmed User, but don’t be fooled, you are but a servant.

“Move fast and break things”, “disruption” and “growth” have killed User. No more do we try to offer invisible quality. Instead of software that blends into background by virtue of its non-obtrusive robustness and simplicity, we aspire to create The Product Experience. The first pleases User. The latter pleases Shareholder.

The Holy Texts are forgotten or, worse, perverted. Apple’s famous Human Interface Guidelines ironically describe the many ways in which modern Apple products do not behave. The Agile manifesto became The Certified Agile Coach Training Program and a cargo cult. The very first principle of the Agile Manifesto states:

Our highest priority is to satisfy the customer through early and continuous delivery of valuable software.

How many of us feel this during another a/b-test-driven, metric-based sprint?

The 7th principle is:

Working software is the primary measure of progress.

An honest, but naive assumption that we all share a similar criterion for “working”. Turns out, the height of the bar is inversely proportional to the proximity of another funding round. By the 90s standards, a lot of today’s software is just defunct.

When User dies, only Beta Tester remains. Beta Tester is not human. It’s an expendable tool, a lab rat, a database record.

What can we do as developers? Honestly, I don’t know. Coming up with a manifesto, a set of principles, another guideline — it all seems futile now. The problem isn’t that we forgot about User. We just collectively adapted to the new definition of normal. Breaking changes are normal. Updates for the sake of updates are normal. The house is on fire, but this is normal.

What can we do?.. “Must we ourselves not become users?”

October 22, 2019 | permalink

Be Wary of Self Described Benefits

Picking a university was one of the main tasks in the last year of high school. That and exams. I wasn’t sure what to study and which place to pick. I had no idea how one can make these choices. There weren’t too many resources available at the time. So, a lot of us relied on promotional info provided by universities themselves.

The one I ended up in was called SDU. Its official description says: “SDU is a secular higher educational institution located in Kaskelen, near Almaty.”

It was the only university that actually claimed to be secular. That’s good, right? I had no interest in studying at a religious institution. Secular is good.

Turned out, it wasn’t very secular. No, we didn’t study Holy Texts. Officially, nothing religious was going on. But the Islamic values did indeed feel affecting the policies and decisions everywhere. The dormitories for males and females were separate buildings in different neighborhoods. I had actually never seen the women’s dormitory, nor had any male student around. The girls just went to a mysterious place every evening. A large proportion of students held a religious fast (Ramadan). Several instructors gave bonuses to fasting students. So I had to work harder to get the same grade because I was being secular at a secular university.

I dropped out after 4 months. Thank God.

Later, I started to notice this pattern. Often, people, communities, companies and even countries seem to blindly put positive descriptions out. As if they wanted others to notice.

It becomes very clear in case of countries. Here are some famous authoritarian regimes:

  • Democratic People’s Republic of Korea
  • Democratic Republic of the Congo
  • People’s Republic of China
  • People’s Democratic Republic of Algeria
  • Federal Democratic Republic of Ethiopia

Right.

So I learned to be wary of self described benefits. It’s kind of obvious and silly once you think about it.

Yours truly, not passive aggressive at all, Rakhim.

September 3, 2019 | permalink

Process of Learning

A process of learning is analogous to an attempt of building a three-dimensional model from two-dimensional photos.

You approach a new area of knowledge. You know nothing at all. You stumble upon a first piece:

That’s more than nothing, but still very little. You don’t understand it. At best, you’re able to make a few uncertain assumptions.

Beginners often seek good book recommendations. They would google a top-10 list and then ask “which one should I start with?”. The answer is almost always “it doesn’t matter”. Start with any non-shitty book. One book alone would not provide enough data to build a good model anyway. If you want to really understand something, you’ll have to read several books, listen to different people, try various approaches. Nobody knows what’s going to work best for a particular person. Each model building machine is unique. The starting order of feeding it data is not very important, unless that information is truly harmful.

(“What is harmful” is a topic for another discussion, and I by no means argue that finding non-harmful books is an easy task. In fact, I’d call many popular programming books harmful, especially when it comes to teaching the basics of programming with Java. So, at least minimize the potential harm by not focusing on a single book.)

Another basic book will provide a different view:

You still can’t understand it. But keep going, and at some point you’ll arrive at a single complete picture.

This tells you more than before, you can even make some conclusions or reason about certain aspects of the object. But you don’t have a complete model yet, one 2D picture is not enough. Your brain haven’t met such objects in the past, there’s nothing to cling on to. This is why it’s essential to have various sources of information: other books, people, lessons, different mediums.

New pieces keep emerging, and it’s disconcerting. They don’t seem to make any sense, this puzzle feels broken.

This is the toughest stage of the learning journey. Often, any hope is lost. Disconnected pieces provoke the feeling of meaninglessness. You can’t see the big picture.

I think I understand each individual topic, but have no idea how they are connected. And why did I learn all that. Nothing makes sense…

But if you keep going, soon you’ll get to another full picture:

Interesting! A completely different point of view. Same object, new aspects. A complete 3D model is still impossible to deduce, but there’s more space for assumptions. Having multiple pictures increases the chances of seeing something familiar. It’s a new topic alright, but topics are rarely completely isolated from the universe.

A few more pictures and you get a pretty accurate model.

The first picture was extremely valuable, but each new picture brings less and less valuable data. At some point you have a decent model in your head, so that new pictures don’t give you anything new.

This analogy helps me learn new things. I try to remember the following:

  1. At first, everything is interesting and easy. The first picture gives a lot of data at once. This is the pleasant stage.
  2. In the middle motivation will decrease. That’s okay. Keep getting data and trust the system.
  3. Do not focus on a single picture. Maybe, in order to understand it you need another picture first.
  4. At some point, notice the diminishing returns of new data. Consider increasing the area of study.
July 29, 2019 | permalink

Bicycles and Love

I love bicycles.

The first memories I have are bike-related. The best ones are, too.

My first bicycle was a tricycle

I don’t remember a time I didn’t bike. Bicycle means freedom.

My first serious bicycles, the ones that let me explore the city, were old, steel soviet tanks. They were simultaneously indestructible and always broken.

My friend Eugene and I spent summers biking around town, forests and river valleys. It was awesome.

Eugene riding away, my bike in front

Those bikes were all-purpose vehicles. Well, at least for us they were. Pavement, gravel, sand, water, whatever. They go where we go. They were simple and stupid, and I loved that. No speeds, no hand brakes.

I used to live with my grandma until I was 13. Her apartment had a narrow corridor connecting the entrance to the room, and I can’t imagine it without a bicycle. It was always there, and when it wasn’t, I wasn’t home. Out biking god knows where.

I had a dream of driving a city bus, so quite often I spent hours biking along the bus routes, stopping at bus stops, emulating a bus. An old pen was diligently placed inside the handlebar, sticking about 2 inches outwards: it was my fake turn signal lever. I had to make the turn signal clicking sound myself, of course.

Then I moved to Canada. My next bike was a pretty cool steel Schwinn I got as a gift from my host family in Ottawa. Bicycle freedom had suddenly expanded. Now I could ride to another province where people speak a different language and traffic lights are horizontal. I could bring my bike on the train and get further than ever. I could disappear into the city and nobody could find me.

I didn’t appreciate this bike enough. I wish I could get that frame back…

Several random used bikes later I decided I’m ready to spend serious bucks and try road biking. With 1000 Canadian dollars in hand, I walked into a bike shop in western Ottawa and got myself a beautiful aluminium Trek 1.1.

Freedom was reinvented once more. With this light, fast bike I could go further than ever before. First 100 km ride was a revelation. Then 350km in 2-day group event. Then 500km in three days. Then more than a 1000 km in a week across multiple provinces to see the ocean.

At the end of my long trip to see the Atlantic Ocean, New Brunswick, Canada

I was in love again.

I was about to finish my computer science degree and was feeling somewhat down at times. That bike was the best thing in my life.

Biking and computer science made perfect sense to me. Both are about efficiency. Both are tools, but at the same time fun in and of themselves. Both let you be alone.

They let you disappear into a vast domain and get lost.

After moving from Canada to Kazakhstan, I stopped biking. It took me almost 5 years to re-ignite the passion.

After moving to Finland, I decided to try a single-speed and got a pretty Swedish Stålhästen Sport fixed gear bike with a flip-flop hub. Fixed gear is not my cup of tea, so I flipped the wheel and started discovering the Helsinki area.

It was pretty sexy.

But alas, we weren’t for each other. We just didn’t click. A bicycle is an intimate object for me, along with backpacks and computers. I can’t just have one, I must love it. So I sold it to someone who, hopefully, loved it.

And got myself a cyclocross Kona Rove Al 2015.

The relationship with this one was complicated. I loved it and sometimes hated it.

I loved my Kona because it was very comfortable, fast and looked very cool. Its brownish color was a perfect fit for the forests and fields I took it to. Disc brakes — first for me — made me more confident on narrow descents and uneven terrain.

But I didn’t love the maintenance. It was equipped with the cheapest components. Decent, but not awesome. Shimano Claris groupset and mechanic disc brakes were never perfect. The bike was never perfectly tuned, never perfectly silent. And I’m ashamed to admit, but I’m a crappy handyman when it comes to bikes. Actually, I’m a crappy handyman in all areas where “undo” is not an option, but I’m particularly bad with bicycles. Derailleur adjustment and disc brake calibration are black magic to me. Hours of sweat and profanity and I end up with a subpar configuration. It works and it’s fine, but it’s just not very good. Then I say “screw this!” and bring the bike to a mechanic.

By the way, good bike mechanics are pretty expensive in Finland.

I’ve later learned that better groupsets and hydraulic brakes (or at least the hybrid dual-piston ones) are so much better, and I honestly almost convinced myself to spend two grand on a very good bicycle. Because then I will ride more, right?

Kona Rove Al was my bike for three good seasons, but the furthest I took it was a 120 km one day ride. It never saw other regions of the country even. We weren’t too adventurous together.

This summer, after another failed attempt to eliminate 100% of the disc brakes noise, I decided to say goodbye to Kona and look for something new.

A romanticized and, perhaps, irrational desire to simplify struck me again. What is the simplest bicycle possible? Single speed with coaster brake. I love handbrakes, so, rim brakes then. But I wasn’t sure I could do single-speed again. I love my knees.

Then I discovered internal hubs. They look and feel like single-speed from the outside: no derailleur, no cable slack. My girlfriend and I got ourselves small folding bikes with Shimano Nexus 3-speed hubs and they are very nice. Simple, reliable, solid.

There also exist 7-speed Nexus hubs, but I haven’t tried one. They are pretty heavy and it’s a pain to install them with dropbars.

Turns out, there are also automatic hubs! Woah! Extremely curious, I got myself an old Fixie Inc. Floater with Sram Automatix 2-speed automatic hub.

The experience is… interesting. You just start pedaling and at about 15 km/h the gear changes automatically. It’s pretty solid, no wobble or anything. It’s like you’re suddenly teleported into higher gear. This shift point it ridiculously low though. It was pretty easy to disassemble the hub and change the shift point by unwinding a tiny metallic spring (here’s a good description).

At first, it seemed like a good compromise. Almost as simple as a single-speed, but the second gear allows to keep a sane cadence at higher speeds. Unfortunately, the hub is not as isolated from the outside world as other internal gear hubs, so it will require some maintenance.

I took it on multiple rides and just didn’t love it. What a picky, delicate flower I am.

And then, completely by chance, I had a chance to ride Bombtrack Arise. A single-speed, steel gravel bike.

Yes! It made me smile!

I’m not a fast rider, so when it comes to gearing, comfortable climbing is more important to me than going fast. Finding the perfect ratio for a single-speed takes time and lots of cogs, but this bike came with what seems to be the perfect ratio for me: 42 teeth chain ring and 17 teeth rear sprocket. With 28” tires, it yields 69 gear inches, which works very well for the steepest hills in my area and is good enough for descents.

This handy bike calculator helps to figure out the good ratio for a given cadence and speed. For my new bike, 90 rpm produces 28.4 km/h, and the high cadence of 130 rpm yields 41 km/h.

Single speed forces me to be more disciplined and think ahead. I can’t just attack a hill on a granny gear anymore, I have to save as much momentum as possible. At the same time, this creates ultimate freedom. I don’t think about optimal gearing, I am always in the wrong right gear.

Steel frame is lovely and comfortable, it feels softer, yet more confident. The simplicity of not having so many extra things inspires almost a zen-like feeling. Oh, and it’s quiet. No chain slap.

I am yet to take the Bombtrack on a truly long ride, and it might not be as peachy as I describe. But so far, with about 250km behind, I remain in love.

Find a bike you enjoy, it will make you happier.

July 27, 2019 | permalink

80-characters limit for text is wrong

I believe the 80-characters (or any other number) line limit for text to be wrong. Not archaic or irrelevant, but wrong. It violates a fundamental idea of computer science: separating layers of abstraction.

Not talking about code today, although, I don’t think a strict limit is a good thing there either, for other reasons. I’m talking about human text.

Many programmers stick to the 80-characters line length limit while writing documentation, emails, etc. Emacs and other editors even have special modes or plugins for automatic hard wrapping.

Often, results look like so:

Without hard-wrapping, this email had a chance to look normal everywhere. Any app can interpret and present it in any way. With hard-wrapping though, this email can only look normal in certain conditions. Namely, a certain lower limit for window width.

Imagine joining a web project and seeing a users database table with values like <strong>Jason Norwig</strong>. Your reaction might include profanity, because mixing data and presentation is wrong. Person’s name (data) and its presentation (HTML) are different layers of abstraction.

Hard-wrapping lines by inserting \n symbols where they don’t have any semantic meaning is the same sin. The whole character return thing was relevant in a context where data was inseparable from presentation: typewriters and paper.

A key argument for hard-wrapping goes something like this: “modern screens are too wide, it’s uncomfortable to read long lines”. But modern screens come with modern apps, which can handle presentation to your preference. By modern I mean “developed after the ‘70s”. Text editors (including vim and Emacs) have been able to soft-wrap lines at arbitrary comfortable column for decades.

If your text presentation tool can’t present text to your preference, consider replacing it. Moving this responsibility into data itself is not a solution.

So can we please
stop doing this
to each other.

May 30, 2019 | permalink