Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

     

100 Languages Speedrun: Episode 67: Io

Smalltalk was the original minimalist object-oriented language. As everything about Smalltalk was so modifiable, it didn't remain a single language, instead spawning huge number of incompatible descendants, each trying their different ideas. Some kept the Smalltalk names, others did not.

Io is one of such descendants. The main difference are prototype-based rather than class-based object-oriented system, and even more minimal grammar.

Hello, World!

Io can be ran from Unix scripts, and that's what I'll be doing. Here's the Hello, World:

#!/usr/bin/env io"Hello, World!\n" print
Enter fullscreen modeExit fullscreen mode

We construct a"Hello, World!\n" string object, and send it a message toprint itself, which it does.

$ ./hello.ioHello, World!
Enter fullscreen modeExit fullscreen mode

Smalltalk terminated the statements with. which wasn't exactly an improvement over;. Io figured out newlines are perfectly fine statement terminators, so it's much clearer.

Io also hasprintln message for printing something with a newline.

We can also runio from command line to get REPL, but it doesn't have proper line editing, so I recommend running it withrlwrap io.

Math

Io has normal operator precedence, so none of the Smalltalk's silliness where it would just go left to right ignoring established mathematical convention in name of "simplicity". Most Smalltalk descendants figured out that was one of Smalltalk's dumber ideas.

#!/usr/bin/env ioa := 2b := 3c := 4(a + b * c) println
Enter fullscreen modeExit fullscreen mode

As expected it prints correct 14 not naively-left-to-right 20:

$ ./math.io14
Enter fullscreen modeExit fullscreen mode

You can also define your own new operators and assign them precedence levels, but it won't apply to the current file (files are parsed before being executed).

FizzBuzz

Smalltalk does control structures with blocks. Io can do that, but there are also other ways.

Unlike pretty much every other language, arguments in Io are not evaluated automatically, and the called function needs to decide if it wants to evaluate them or not.

Let's try to write a simple FizzBuzz program:

#!/usr/bin/env io# FizzBuzz in IoNumber fizzbuzz := method(  if(self % 15 == 0, "FizzBuzz",    if(self % 5 == 0, "Buzz",      if(self % 3 == 0, "Fizz",        self))))for(i, 1, 100,  i fizzbuzz println)
Enter fullscreen modeExit fullscreen mode

Here's a completely different one:

#!/usr/bin/env ioNumber fizzbuzz := method(  (self % 15 == 0) ifTrue (return "FizzBuzz")  (self % 5 == 0) ifTrue (return "Buzz")  (self % 3 == 0) ifTrue (return "Fizz")  self)100 repeat(i, (i+1) fizzbuzz println)
Enter fullscreen modeExit fullscreen mode

Let's go through what's going on step by step:

  • Number fizzbuzz := method(...) adds a methodfizzbuzz to prototype ofNumber
  • Prototype ofNumber is0, obviously. If you doNumber + 69 you get69. There are no classes in Io.
  • if(condition, thenBranch, elseBranch) does not evaluate itsthenBranch andifBranch before it can figure out thecondition - everything in Io has this evaluation model.
  • ifTrue (code) andifFalse (code) are more Smalltalk-style methods. They'll run the code or not depending on which object you send it to -true orfalse
  • Number repeat() is one way of looping - but it starts from 0 so we need to add 1 to the counter.
  • for(i, 1, 100, ...) is another way of looping.
  • methods have more traditional names and arguments, Smalltalk convention of having names be lists of their keyword argument (likeifTrue:ifFalse:) is gone

Fibonacci

This code is a treat:

#!/usr/bin/env ioNumber fib := method((self-2) fib + (self-1) fib)1 fib := method(1)2 fib := method(1)for(i, 1, 30, "fib(#{i}) = #{i fib}" interpolate println)
Enter fullscreen modeExit fullscreen mode

We definefib onNumber prototype to be(self-2) fib + (self-1) fib. Then since Io doesn't have classes, we casually redefine it on objects1 and2 to be our base case.

The we loop. Io doesn't support string interpolation, but due to its lazy evaluation,String interpolate method can do all the interpolating for us!

$ ./fib.iofib(1) = 1fib(2) = 1fib(3) = 2fib(4) = 3fib(5) = 5fib(6) = 8fib(7) = 13fib(8) = 21fib(9) = 34fib(10) = 55fib(11) = 89fib(12) = 144fib(13) = 233fib(14) = 377fib(15) = 610fib(16) = 987fib(17) = 1597fib(18) = 2584fib(19) = 4181fib(20) = 6765fib(21) = 10946fib(22) = 17711fib(23) = 28657fib(24) = 46368fib(25) = 75025fib(26) = 121393fib(27) = 196418fib(28) = 317811fib(29) = 514229fib(30) = 832040
Enter fullscreen modeExit fullscreen mode

Unicode

Io can correctly see lengths of Unicode strings, but somehow cannot convert them to upper or lower case.

#!/usr/bin/env io"Hello" size println"Żółw" size println"🍰" size println"Żółw" asUppercase println"Żółw" asLowercase println
Enter fullscreen modeExit fullscreen mode
$ ./unicode.io541ŻółWŻółw
Enter fullscreen modeExit fullscreen mode

This is not something Smalltalk had any idea about, as it predates Unicode by decades, but Io should know better, and it failed here.

Lists

Io doesn't have any special syntax for lists, butlist(...) method works, and it comes with the usual methods, with more modern Ruby-style naming (map andreduce; notcollect andinject):

#!/usr/bin/env ioa := list(1, 2, 3, 4, 5)a map(x, x * 2) printlna select(x, x % 2 == 0) printlna reduce(x, y, x + y) printlna at(0) printlna at(-1) println
Enter fullscreen modeExit fullscreen mode
$ ./lists.iolist(2, 4, 6, 8, 10)list(2, 4)1515
Enter fullscreen modeExit fullscreen mode

Maps

Io of course has maps (also known as hashes, or dictionaries, or objects etc. - why can't all languages just agree on a single name), but these are quite awkward:

#!/usr/bin/env ioa := Map clonea atPut("name", "Alice")a atPut("last_name", "Smith")a atPut("age", 25)"""#{a at("name")} #{a at("last_name")} is #{a at("age")} years old""" interpolate printlna printlna asJson println
Enter fullscreen modeExit fullscreen mode
$ ./maps.ioAlice Smith is 25 years old Map_0x7f9c840998b0:{"last_name":"Smith","age":25,"name":"Alice"}
Enter fullscreen modeExit fullscreen mode

A few things to note here:

  • because string interpolation is not part of Io syntax, we cannot just use" inside#{} blocks like we could in Ruby - the workaround is triple-quoting the outside string in such cases, and once-quoting the inner strings. Io doesn't support single quotes for strings either, so that wouldn't work.
  • defaultMap print is useless
  • most Io objects come with usableasJson - but somehow there's no way to parse JSON included in Io! That's really weird.

Point prototype

There are no classes in Io - instead we just have prototypes we can clone.

#!/usr/bin/env ioPoint := Object clonePoint x := 0Point y := 0Point + := method(other,  result := self clone  result x := self x + other x  result y := self y + other y  return result)Point asString := method(return "Point(#{self x}, #{self y})" interpolate)a := Point clonea x := 60a y := 400b := Point clone do(  x := 9  y := 20)a printlnb println(a + b) println"Slots of Object prototype: #{Object slotNames}" interpolate println"Slots of Point prototype: #{Point slotNames}" interpolate println"Slots of individual point: #{a slotNames}" interpolate println
Enter fullscreen modeExit fullscreen mode
$ ./point.ioPoint(60, 400)Point(9, 20)Point(69, 420)Slots of Object prototype: list(pause, hasSlot, coroFor, serializedSlotsWithNames, not, continue, markClean, removeSlot, >=, appendProto, in, memorySize, actorProcessQueue, setIsActivatable, isIdenticalTo, hasProto, newSlot, justSerialized, thisLocalContext, , slotDescriptionMap, addTrait, print, argIsCall, while, ifNilEval, argIsActivationRecord, evalArg, prependProto, message, write, asSimpleString, <=, setSlot, inlineMethod, lazySlot, ancestors, thisMessage, init, ifNil, futureSend, if, doRelativeFile, serialized, become, isTrue, getSlot, foreachSlot, perform, returnIfNonNil, type, ifNonNil, ancestorWithSlot, for, isKindOf, slotValues, evalArgAndReturnNil, asBoolean, raiseIfError, shallowCopy, method, .., ==, deprecatedWarning, ifNonNilEval, returnIfError, <, doFile, asyncSend, clone, list, ifError, removeAllProtos, stopStatus, uniqueId, doString, apropos, super, block, isNil, evalArgAndReturnSelf, coroDoLater, isActivatable, launchFile, >, slotNames, isLaunchScript, setSlotWithType, and, break, @, try, performWithArgList, loop, -, setProto, switch, asString, uniqueHexId, actorRun, !=, proto, getLocalSlot, lexicalDo, removeAllSlots, coroDo, slotSummary, removeProto, compare, wait, do, coroWith, ?, cloneWithoutInit, relativeDoFile, contextWithSlot, currentCoro, protos, isError, @@, resend, serializedSlots, return, hasDirtySlot, thisContext, handleActorException, or, yield, updateSlot, writeln, hasLocalSlot, println, ownsSlots, doMessage, setProtos)Slots of Point prototype: list(x, type, y, asString, +)Slots of individual point: list(x, y)
Enter fullscreen modeExit fullscreen mode
  • we start byObject clone to get a new object with all the usual stuff defined on it
  • then we addd some slots to thePoint prototype, namelyx andy with default values,+ operator, andasString method
  • to create a newPoint we doPoint clone, then update any slots we want to change - there's no real difference between overriding instance variables and methods, we can overridea asString to say"Nice Point" as easily as overridinga x to move it somewhere else
  • Io doesn't really have keywords and such, all the basic functionality is implemented as methods onObject prototype - as you can see the list is very long
  • any method not defined by the object will be called on its prototype

More OO

Io OOP does very little for us.clone callsinit, so if we need to do some object initialization we can do it there, but it's not meant as a constructor, it's mainly soclone can alsoclone any instance variables that need it.

The closest to a "constructor" Io has is a convention ofwith(arguments) method closing the receiver and calling various setters onarguments.

Here's another, and much more concise, implementation ofPoint:

#!/usr/bin/env ioPoint := Object clone do(  x ::= 0  y ::= 0  with := method(xval,yval,self clone setX(xval) setY(yval))  asString := method(return "Point(#{self x}, #{self y})" interpolate)  + := method(other, return self with(x + other x, y + other y)))a := Point with(60, 400)b := Point with(9, 20)(a+b) println
Enter fullscreen modeExit fullscreen mode
$ ./point2.ioPoint(69, 420)
Enter fullscreen modeExit fullscreen mode
  • do(...) is sort of like Ruby'sinstance_eval, code will be executed in context of whichever object we called it on
  • ::= is a shorthand for:=, but it also defines setters (setX andsetY)
  • setters return the original object, so they can be chained likeaPoint setX(x) setY(y)
  • with is just a convention, but very useful one, everything is so concise now

Autoloading

Another nice thing Io does is it doesn't pollute your programs withimport statements.

Let's say we have thislorem.io:

Lorem := "Lorem ipsum dolor sit amet, consectetur adipiscing elit,sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis nostrud exercitation ullamco laborisnisi ut aliquip ex ea commodo consequat. Duis aute irure dolor inreprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident,sunt in culpa qui officia deserunt mollit anim id est laborum."
Enter fullscreen modeExit fullscreen mode

And then we run thisprint_lorem.io:

#!/usr/bin/env ioLorem println
Enter fullscreen modeExit fullscreen mode

If we run it:

$ ./print_lorem.ioLorem ipsum dolor sit amet, consectetur adipiscing elit,sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis nostrud exercitation ullamco laborisnisi ut aliquip ex ea commodo consequat. Duis aute irure dolor inreprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident,sunt in culpa qui officia deserunt mollit anim id est laborum.
Enter fullscreen modeExit fullscreen mode

Any unknown constant gets autoloaded, Ruby on Rails style. You can configure the paths etc.

This cuts on stupid boilerplate so much, I have no idea why this Ruby on Rails innovation didn't spread everywhere yet. Even most new languages start every file with pile of stupid import boilerplate code.

Io evaluation model

Let's get to the most interesting thing about Io - its evaluation model. Unlike Smalltalk, Io doesn't have any block in the syntax. That's because everything is a block.

If you doa atPut("name", "Alice"), you're not actually sending a message withatPut with two values"name", and"Alice" as arguments, like you would in Ruby.

What you're actually sending is a messageatPut with two code blocks! It's up to you to send them back to the sending context with "please evaluate them for me".

Now as this is such a common 99% case, Io will do this part for you if you define any named arguments to the method. So if you definemethod(a, b, c, code), the first three arguments will be evaluated and assigned toa,b, andc. Any arguments you did not define won't be evaluated.

Here's some fun code:

#!/usr/bin/env ioDate dayOfWeek := method(self asString("%A") asLowercase)onDay := method(  arg0 := call message argAt(0) asString  arg1 := call message argAt(1)  day := Date now dayOfWeek  (day == arg0) ifTrue(call sender doMessage(arg1)))onDay(monday, "I love Mondays!" println)onDay(tuesday, "Tuesdays are allright too I guess..." println)
Enter fullscreen modeExit fullscreen mode

First, we need to defineDate dayOfWeek to return string like"monday","tuesday", etc. Io standard library is quick lacking overall.

Then we add new operator to the language,onDay(day, code). It will runcode on specific day of the week. As we didn't specify any arguments tomethod(argument0, argument1, code), it will not evaluate them.

We can extract these arguments withcall message argAt(0) etc., as code blocks. Then we can convert them to strings withasString. Notice we didn't need to do anymonday - it's not a real method, it's just a syntax we created foronDay.

We can also tell the caller to evaluate them withcall sender doMessage(arg1).

The wholecall sender doMessage(call message argAt(1)) can also be much more concisely expressed ascall evalArgAt(1).

$ ./week.ioTuesdays are allright too I guess...
Enter fullscreen modeExit fullscreen mode

Testing Library

Let's do something more useful and build a testing library! This is one thing where even Ruby struggles a bit, as RSpec needs to be a bit awkwardexpect(something).to eq(expected).

Io has so much syntax flexibility, we should have no problem with this.

#!/usr/bin/env ioassertEqual := method(left, right,  (left == right) ifFalse(    leftExpr := call message argAt(0) asString    rightExpr := call message argAt(1) asString    "assertion failed:\n  #{leftExpr} # => #{left}\ndoes not equal\n  #{rightExpr} # => #{right}" interpolate println  ))# Io uses normal mathassertEqual(2 + 2 * 2, 6)# Io does not use Smalltalk mathassertEqual(2 + 2 * 2, 8)
Enter fullscreen modeExit fullscreen mode

This is surprisingly nice, except spacing of the blocks wasn't preserved:

$ ./assert_equal.ioassertion failed:  2 +(2 *(2)) # => 6does not equal  8 # => 8
Enter fullscreen modeExit fullscreen mode

Unfortunately when I tried to use this on strings, the whole thing fell apart:

#!/usr/bin/env ioassertEqual := method(left, right,  (left == right) ifFalse(    leftExpr := call message argAt(0) asString    rightExpr := call message argAt(1) asString    "assertion failed:\n  #{leftExpr} # => #{left}\ndoes not equal\n  #{rightExpr} # => #{right}" interpolate println  ))# Ascii worksassertEqual("hello" asUppercase, "HELLO")# Sadly no UnicodeassertEqual("żółw" asUppercase, "ŻÓŁW")
Enter fullscreen modeExit fullscreen mode
$ ./assert_equal2.ioassertion failed:  " asUppercase # => |�BWdoes not equal  " # => {�AW
Enter fullscreen modeExit fullscreen mode

We already know Io doesn't fully support Unicode, but I thought it would at least be able to print Unicode strings.

Better testing library

I also tried to do something more:

#!/usr/bin/env ioassert := method(comparison,  (comparison) ifFalse(    code := call message argAt(0)    "This code failed: #{code}" interpolate println  ))# Io uses normal mathassert(2 + 2 * 2 == 6)assert(2 + 2 * 2 > 5)assert(6 == 2 + 2 * 2)# Io does not use Smalltalk mathassert(2 + 2 * 2 == 8)assert(2 + 2 * 2 > 7)assert(8 == 2 + 2 * 2)
Enter fullscreen modeExit fullscreen mode

If Io was like Lisp or Ruby, we'd be able to get the block and see the top level operator, and its arguments, that is splitting2 + 2 * 2 == 8 into2 + 2 * 2,==, and8 - this would enable us to have some really nice testing library with great messages.

Unfortunately there doesn't seem to be any way to dig into syntax tree in Io. I can access raw text of the block, and I can run the block, and it looks like I can get it token by token, but no parse tree. That doesn't make Io bad, it's just a "we were so close to greatness" moment.

Square Brackets

Interestingly Io defines overloadable operators even for characters it doesn't actually use. For example if you use[] in your Io code, you get an error thatsquareBrackets is not recognized. So let's define it!

#!/usr/bin/env iosquareBrackets := method(  result := list()  call message arguments foreach(item, result append(doMessage(item)))  return result)array := [1, 2, 3+4, ["foo", "bar"]]array asJson println
Enter fullscreen modeExit fullscreen mode

In this case we usecall message arguments not because we do any crazy metaprogramming, but just to support variable number of arguments.

This is something more languages should consider doing. For example Ruby could supportdef <<< etc. for those objects which just really need a few extra symbols. Then again, it might want to keep its future syntax options open, so I understand why it's not doing it.

Matrix

And after all the toy examples, something more substantial, a smallMatrix class, for NxM matrices.

#!/usr/bin/env ioMatrix := Object cloneMatrix init := method(  self contents := list()  self xsize := 0  self ysize := 0)Matrix dim := method(x,y,  contents = list()  xsize = x  ysize = y  for(i,1,x,    row := list()    for(j,1,y, row append(0))    contents append(row))  self)Matrix rangeCheck := method(x,y,  if(x<1 or y<1 or x>xsize or y>ysize,    Exception raise("[#{x},#{y}] out of bonds of matrix" interpolate)))Matrix get := method(x,y,  rangeCheck(x,y)  contents at(x-1) at(y-1))Matrix set := method(x,y,v,  rangeCheck(x,y)  contents at(x-1) atPut(y-1,v))Matrix asString := method(  contents map(row,    "[" .. (row join(" ")) .. "]") join("\n"))Matrix foreach := method(  # like method(xi,yi,vi,blk,...)  # except we do not want to evaluate it  args := call message arguments  xi := args at(0) name  yi := args at(1) name  vi := args at(2) name  msg :=  args at(3)  ctx := Object clone  ctx setProto(call sender)  for(i,1,xsize,    for(j,1,ysize,      ctx setSlot(xi, i)      ctx setSlot(yi, j)      ctx setSlot(vi, get(i,j))      msg doInContext(ctx))))Matrix transpose := method(  result := Matrix clone dim(ysize, xsize)  foreach(x,y,v,result set(y,x,v))  result)Matrix saveAs := method(path,  file := File open(path)  file write(asString, "\n")  file close)Matrix loadFrom := method(path,  file := File open(path)  lines := file readLines map(line,    line removeSuffix("\n") removeSuffix("]") removePrefix("[") split(" ") map(x, x asNumber))  ysize := lines size  xsize := lines at(0) size  result := Matrix clone dim(xsize, ysize)  for(i,1,xsize,    for(j,1,ysize,      result set(i,j,lines at(j-1) at(i-1))))  result)newMatrix := Matrix clone dim(2,3)newMatrix printlnnewMatrix contents printlnnewMatrix set(1, 1, 10)newMatrix set(1, 2, 20)newMatrix set(1, 3, -30)newMatrix set(2, 1, 15)# (2,2) defaults to 0newMatrix set(2, 3, 5)"Matrix looks like this:" printlnnewMatrix println"\nPrinted cell by cell:" printlnnewMatrix foreach(a,b,c,  ("Matrix[" .. a .. "," .. b .. "]=" .. c) println)"\nTransposed:" printlnnewMatrix transpose printlnnewMatrix saveAs("matrix.txt")matrix2 := Matrix loadFrom("matrix.txt")"\nLoaded:" printlnmatrix2 printlnmatrix2 get(69,420) println
Enter fullscreen modeExit fullscreen mode
./matrix.io[0 0 0][0 0 0]list(list(0, 0, 0), list(0, 0, 0))Matrix looks like this:[10 20 -30][15 0 5]Printed cell by cell:Matrix[1,1]=10Matrix[1,2]=20Matrix[1,3]=-30Matrix[2,1]=15Matrix[2,2]=0Matrix[2,3]=5Transposed:[10 15][20 0][-30 5]Loaded:[10 15][20 0][-30 5]  Exception: [69,420] out of bonds of matrix  ---------  Exception raise                      matrix.io 20  Matrix rangeCheck                    matrix.io 22  Matrix get                           matrix.io 96  CLI doFile                           Z_CLI.io 140  CLI run                              IoState_runCLI() 1
Enter fullscreen modeExit fullscreen mode

I won't get too in-depth, but here's some highlights:

  • we needMatrix init to setcontents, otherwise we'd share storage with parent matrix
  • Matrix rangeCheck shows how exceptions work in Io - we raise one by invalid operation on the final line
  • Matrix foreach shows how we can do complex block programming without any special support by the language - we create new context object, set slots there, and evaluate it withdoInContext - because caller is its prototype we get full access to caller's context as well!
  • Matrix saveAs andMatrix loadFrom show file I/O

Forwarding

Like in any real OOP language, we can do simple proxy:

#!/usr/bin/env ioCat := Object cloneCat meow := method("Meow!" println)Cat asString := "I'm a Kitty!"Spy := Object cloneSpy object := CatSpy forward := method(  m := call message name  args := call message arguments  "Someone's trying to ask #{object} to #{m} with #{args}" interpolate println  object doMessage(call message))Cat meowSpy meow
Enter fullscreen modeExit fullscreen mode
$ ./forward.ioMeow!Someone's trying to ask I'm a Kitty! to meow with list()Meow!
Enter fullscreen modeExit fullscreen mode

Class-based OOP vs Prototype-based OOP

For a while there were two kinds of genuine OOP - class-based and prototype-based - as well far more popular as half-assed OOP of Java variety, which I won't mention here.

Descendants of Smalltalk are also split between class-based (anything that kept the name "Smalltalk") and prototype-based (Io, Self).

It was hard to tell which one was better, until a large scale "natural experiment" happened, and millions of programmers were forced to experience prototype-based OOP in JavaScript whether they liked it or not. And I have trouble recalling any concept in programming that was more soundly and universally rejected. Every JavaScript framework pre-ES6 had its own half-assed class-based OOP system, as literally anything was better than using prototype-based OOP. CoffeeScript's main selling point were the classes, and it was on track to replace JavaScript for a while, before it copied that too. And since ES6, everyone switched to classes (or to pseudo-functional programming like React Hooks) with nobody looking back to the prototypes, still technically being in the language. The question is absolutely solved - class-based OOP is absolutely superior to prototype-based OOP. It's an empirically established fact by now.

Prototype-based OOP is still interesting esoteric way to program, it's just important to acknowledge lessons learned.

Should you use Io?

There's a reason why this is one of the longest episodes yet, as Io is really fascinating.

But I wouldn't recommend it for anything serious, Io is seriously lacking a lot of practical features all across the board. The language is quite elegant, but the standard library is extremely lacking, and very inconsistently designed. It also looks like it's not really actively developed anymore.

But at least it's trying to do a modern take on Smalltalk. All others I tried, starting with GNU Smalltalk, were hopelessly stuck in the past. Many other Smalltalks and descendants I tried wouldn't even install or run on modern systems, so just working withbrew install io, and dropping most of the silly historical baggage makes Io likely the best Smalltalk-like language of today (not counting more distant descendants like Ruby, JavaScript etc.).

I think Smalltalk-land is in much worse shape than Lisp-land where Racket and Clojure are reasonable languages to use. I'd only recommend Io as a language to play with, but that's still more than any other Smalltalk-like.

Io could be turned into an interesting small language if someone spent some time making its standard library not awful, added package manager, and so on, but that's very speculative.

Code

All code examples for the series will be in this repository.

Code for the Io episode is available here.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Open Source hacker. I like Ruby and cats.
  • Location
    London
  • Education
    Wrocław University
  • Joined

More fromTomasz Wegrzanowski

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp