[220122] java PushbackReader

Table of Contents

1 동기

clojureread-string 은 내부적으로 java.io.PushbackReader 를 사용한다. 이것이 무엇을 하는지 알아보는 시간을 가지자.

2 도큐먼트

https://docs.oracle.com/javase/7/docs/api/java/io/PushbackReader.html

java.io.PushbackReader 는 말그대로 PushBack이 가능한 Reader이다. 읽은 값을 다시 맨 뒤에 붙여서 다음에 또 읽을 수도 있고, 그대로 스킵할 수도 있다.

관련 메서드만 보면 대충 어떤 일을 하는 지 알 수 있다.

  • read() : 하나의 문자를 읽는다.
  • unread(inc ) : 문자하나를 푸시백 버퍼 앞에 복사하여 푸시백을 수행한다.
  • skip(long n) : 문자들을 스킵한다.

3 사용법

3.1 Without Pushback

import java.io.CharArrayReader;
import java.io.IOException;
import java.io.PushbackReader;

public class PushBackReaderTest {
    public static void main(String[] args) throws IOException {
	char buf[] = {'a', 'b', 'c', 'd'};
	PushbackReader pr = new PushbackReader(new CharArrayReader(buf));

	while(true) {
	    int c = pr.read();
	    if (c == -1) break;
	    System.out.println((char) c);
	}
    }
}
// returns
// a
// b
// c
// d

이제 pushback을 수행해보자.

3.2 With Pushback

import java.io.CharArrayReader;
import java.io.IOException;
import java.io.PushbackReader;

public class PushBackReaderTest {
    public static void main(String[] args) throws IOException {
	char buf[] = {'a', 'b', 'c', 'd'};
	PushbackReader pr = new PushbackReader(new CharArrayReader(buf));

	while(true) {
	    int c = pr.read();
	    if (c == -1) break;
	    if ('a' == (char) c) {
		pr.unread('A');
	    }
	    System.out.println((char) c);
	}
    }
}
// returns
// a
// A
// b
// c
// d

4 clojure는 어디서 PushBackReader를 사용하는가?

clojure에는 EdnReader.java 에서 readString 메소드를 보자.

static public Object readString(String s, IPersistentMap opts){
    PushbackReader r = new PushbackReader(new java.io.StringReader(s));
  return read(r, opts);
}

static public Object read(PushbackReader r, IPersistentMap opts){
  return read(r,!opts.containsKey(EOF),opts.valAt(EOF),false,opts);
}

그리고 이 두 함수는 아래 아주 긴 read 함수로 수렴한다.

static public Object read(PushbackReader r, boolean eofIsError, Object eofValue, boolean isRecursive,
			  Object opts) {
  try {
    for(; ;) { // while true 라고 보면됨
      int ch = read1(r);
      // whitespace 제거 
      while(isWhitespace(ch)) ch = read1(r);

      if(ch == -1) {
	if(eofIsError) throw Util.runtimeException("EOF while reading");
	return eofValue;
      }

      if(Character.isDigit(ch)) {
	Object n = readNumber(r, (char) ch);
	if(RT.suppressRead()) return null;

	return n;
      }

      IFn macroFn = getMacro(ch);
      if(macroFn != null) {
	Object ret = macroFn.invoke(r, (char) ch, opts);
	if(RT.suppressRead()) return null;
	//no op macros return the reader
	if(ret == r) continue;

	return ret;
      }

      if(ch == '+' || ch == '-') {
	// 잠깐 다음 토큰을 가져와본다. 
	int ch2 = read1(r);
	// 그 값이 숫자라면? 숫자가 두자리수가 있을 수 있으니 
	// 1. 일단 pushback을 한다.
	// 2. readNumber로 숫자를 읽는다. 
	if(Character.isDigit(ch2)) {
	  unread(r, ch2);  // 여기서 pushback을 사용한다. 숫자인 경우 pushback을 한다. 
	  Object n = readNumber(r, (char) ch);  // ch는 부호로 쓰일 것이다. 
	  if(RT.suppressRead()) return null;
	  // 다 읽으면 그것은 숫자이므로 그대로 리턴한다.
	  // +123 , -443 
	  return n;
	}
      // 숫자가 아니면 숫자가 아니므로 다시 읽은것을 돌려놓는다.
      unread(r, ch2);
      }

      String token = readToken(r, (char) ch, true);
      if(RT.suppressRead()) return null;
      return interpretToken(token);
    }
  }
  catch(Exception e) {
    if(isRecursive || !(r instanceof LineNumberingPushbackReader))
      throw Util.sneakyThrow(e);
    LineNumberingPushbackReader rdr = (LineNumberingPushbackReader) r;
    //throw Util.runtimeException(String.format("ReaderError:(%d,1) %s", rdr.getLineNumber(), e.getMessage()), e);
    throw new ReaderException(rdr.getLineNumber(), rdr.getColumnNumber(), e);
  }
}

여기서 우리가 눈여겨 볼 코드는 (위에 적어놓기도 했지만) 숫자를 읽는 코드다.

if(ch == '+' || ch == '-') {
// 잠깐 다음 토큰을 가져와본다. 
int ch2 = read1(r);
// 그 값이 숫자라면? 숫자가 두자리수가 있을 수 있으니 
// 1. 일단 pushback을 한다.
// 2. readNumber로 숫자를 읽는다. 
if(Character.isDigit(ch2)) {
    unread(r, ch2);  // 여기서 pushback을 사용한다. 숫자인 경우 pushback을 한다. 
    Object n = readNumber(r, (char) ch);  // ch는 부호로 쓰일 것이다. 
    if(RT.suppressRead()) return null;
    // 다 읽으면 그것은 숫자이므로 그대로 리턴한다.
    // +123 , -443 
    return n;
}
unread(r, ch2);
}

나는 분명 readNumber 에서 사용하는 ch 는 부호로 쓰일 것으로 예상했다. 아직 이 글을 쓰는 순간에도 해당 함수를 들여다보지 않았다.

이제 들여다보자.

static private Object readNumber(PushbackReader r, char initch) {
  StringBuilder sb = new StringBuilder();
  sb.append(initch);

  for(; ;) {
    int ch = read1(r);
    if(ch == -1 || isWhitespace(ch) || isMacro(ch)) {
      unread(r, ch);
      break;
    }
    sb.append((char) ch);
  }

  String s = sb.toString();
  Object n = matchNumber(s);

  if(n == null)
    throw new NumberFormatException("Invalid number: " + s);
  return n;
}

역시 맨 처음에 들어갈 부호로써 +,- 사용되는 것을 알 수 있다. sb.append(initch)

5 RT.java 의 readString

REPL에서 read-string 을 해보았을 것이다. 단순 문자열을 클로저의 세계로 인도해주는 녀석이 RT#readString 이다.

static public Object readString(String s, Object opts) {
	PushbackReader r = new PushbackReader(new StringReader(s));
	return LispReader.read(r, opts);
}

한번 진짜인지 봐볼까? github으로 clojure 소스코드를 받아서 다음처럼 바꿔보았다.

static public Object readString(String s, Object opts) {
	System.out.println("하하하 여기다!!");
	PushbackReader r = new PushbackReader(new StringReader(s));
	return LispReader.read(r, opts);
}

실행해보자. clojure package에 main.java 를 실행하라.

public static void main(String[] args) {
  RT.init();
  REQUIRE.invoke(CLOJURE_MAIN);
  MAIN.applyTo(RT.seq(args));
}

이후 생성된 REPL에 read-string 을 수행하자.

user=> (read-string "A")
하하하 여기다!!
A

6 EdnReader.java

EdnReader에도 있다. 한번 테스트 해보자.

static public Object readString(String s, IPersistentMap opts){
	System.out.println("HI THIS IS Edn/READSTRING");
	PushbackReader r = new PushbackReader(new java.io.StringReader(s));
	return read(r, opts);
}

repl에서 다음처럼 사용하면 된다.

user=> (require '[clojure.edn :a edn])
user=> (edn/read-string "A")
HI THIS IS READSTRING
A

Date: 2022-01-22 Sat 00:00

Author: Younghwan Nam

Created: 2024-04-16 Tue 10:05

Emacs 27.2 (Org mode 9.4.4)

Validate