dolos 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/setup'
3
+ require 'dolos'
4
+ require 'dolos_common_parsers/common_parsers'
5
+ require 'benchmark/ips'
6
+
7
+ include Dolos
8
+
9
+ # Include common parsers
10
+ # In future this can be more structured, moved them to separate module to prevent breaking changes
11
+ include Dolos::CommonParsers
12
+
13
+ # Library usage example
14
+ # Parse out a name and address from a letter
15
+ # For higher difficulty, we will not split this into multiple lines, but instead parse it all at once
16
+ letter = <<-LETTER
17
+ Mr. Vardeniui Pavardeniui
18
+ AB „Lietuvos Paštas“
19
+ Totorių g. 8
20
+ 01121 Vilnius
21
+ LETTER
22
+
23
+ # Combine with 'or'
24
+ honorific = c("Mr. ") | c("Mrs. ") | c("Ms. ")
25
+
26
+ # Can be parsed any_char which will include needed letters
27
+ # Or combine LT letters with latin alphabet
28
+ alpha_with_lt = char_in("ąčęėįšųūžĄČĘĖĮŠŲŪŽ") | alpha
29
+
30
+ # Capture all letters in a row and join them,
31
+ # because they are captured as elements of array by each alpha_with_lt parser.
32
+ first_name = alpha_with_lt.rep.map(&:join).capture!
33
+ last_name = alpha_with_lt.rep.map(&:join).capture!
34
+
35
+ # Combine first line parsers
36
+ # Consume zero or more whitespace, after that honorific must follow and so on
37
+ name_line = ws_rep0 & honorific & first_name & ws & last_name & eol
38
+
39
+ # Next line is company info
40
+ # We could choose to accept UAB and AB or just AB and etc.
41
+ # 'c("AB")' is for case-sensitive string. 'string' can also be used
42
+ company_type = c("AB")
43
+ quote_open = c("„")
44
+ quote_close = c("“")
45
+
46
+ # Consume LT alphabet with whitespace
47
+ company_name = (alpha_with_lt | ws).rep.map(&:join).capture!
48
+ company_info = company_type & ws_rep0 & quote_open & company_name & quote_close
49
+ second_line = ws_rep0 & company_info & eol
50
+
51
+ # Address line
52
+ # 'char_while' will consume characters while passed predicate is true
53
+ # This could be an alternative to previous 'alpha_with_lt' approach
54
+ # After that result is captured and mapped to hash
55
+ # Mapping to hash so at the end its easy to tell tuples apart
56
+ # Also while mapping, doing some cleaning with '.strip'
57
+ street_name = char_while(->(char) { !char.match(/\d/) }).map { |s| { street: s.strip } }.capture!
58
+ building = digits.map { |s| { building: s.strip } }.capture!
59
+ address_line = ws_rep0 & street_name & building & eol
60
+
61
+ # City line
62
+ # All digits can be matched here or 'digits.rep(5)' could be used. Also joining with map.
63
+ postcode = digits.map { |s| { postcode: s.strip } }.capture!
64
+ city = alpha_with_lt.rep.map(&:join).map { |s| { city: s.strip } }.capture!
65
+ city_line = ws_rep0 & postcode & ws & city & eol
66
+
67
+ # Full letter parser which is combined from all previous parsers. All previous parsers can be ran separately.
68
+ letter_parser = name_line & second_line & address_line & city_line
69
+ result = letter_parser.run(letter)
70
+
71
+ puts result.success?
72
+
73
+ Benchmark.ips do |x|
74
+ x.report('letter benchmark') do
75
+ letter_parser.run(letter)
76
+ end
77
+ x.compare!
78
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require_relative 'dolos'
3
- require_relative 'dolos_common_parsers/common_parsers'
2
+ require 'dolos'
3
+ require 'dolos_common_parsers/common_parsers'
4
4
 
5
5
  include Dolos
6
6
 
@@ -27,12 +27,12 @@ alpha_with_lt = char_in("ąčęėįšųūžĄČĘĖĮŠŲŪŽ") | alpha
27
27
 
28
28
  # Capture all letters in a row and join them,
29
29
  # because they are captured as elements of array by each alpha_with_lt parser.
30
- first_name = alpha_with_lt.rep.capture!.map(&:join)
31
- last_name = alpha_with_lt.rep.capture!.map(&:join)
30
+ first_name = alpha_with_lt.rep.map(&:join).capture!
31
+ last_name = alpha_with_lt.rep.map(&:join).capture!
32
32
 
33
33
  # Combine first line parsers
34
34
  # Consume zero or more whitespace, after that honorific must follow and so on
35
- name_line = ws.rep0 >> honorific >> first_name >> ws >> last_name >> eol
35
+ name_line = ws.rep0 & honorific & first_name & ws & last_name & eol
36
36
 
37
37
  # Next line is company info
38
38
  # We could choose to accept UAB and AB or just AB and etc.
@@ -42,9 +42,9 @@ quote_open = c("„")
42
42
  quote_close = c("“")
43
43
 
44
44
  # Consume LT alphabet with whitespace
45
- company_name = (alpha_with_lt | ws).rep.capture!.map(&:join)
46
- company_info = company_type >> ws.rep0 >> quote_open >> company_name >> quote_close
47
- second_line = ws.rep0 >> company_info >> eol
45
+ company_name = (alpha_with_lt | ws).rep.map(&:join).capture!
46
+ company_info = company_type & ws.rep0 & quote_open & company_name & quote_close
47
+ second_line = ws.rep0 & company_info & eol
48
48
 
49
49
  # Address line
50
50
  # 'char_while' will consume characters while passed predicate is true
@@ -52,18 +52,18 @@ second_line = ws.rep0 >> company_info >> eol
52
52
  # After that result is captured and mapped to hash
53
53
  # Mapping to hash so at the end its easy to tell tuples apart
54
54
  # Also while mapping, doing some cleaning with '.strip'
55
- street_name = char_while(->(char) { !char.match(/\d/) }).capture!.map(&:first).map { |s| { street: s.strip } }
56
- building = digits.capture!.map(&:first).map { |s| { building: s.strip } }
57
- address_line = ws.rep0 >> street_name >> building >> eol
55
+ street_name = char_while(->(char) { !char.match(/\d/) }).map { |s| { street: s.strip } }.capture!
56
+ building = digits.map { |s| { building: s.strip } }.capture!
57
+ address_line = ws.rep0 & street_name & building & eol
58
58
 
59
59
  # City line
60
60
  # All digits can be matched here or 'digits.rep(5)' could be used. Also joining with map.
61
- postcode = digits.capture!.map(&:join).map { |s| { postcode: s.strip } }
62
- city = alpha_with_lt.rep.capture!.map(&:join).map { |s| { city: s.strip } }
63
- city_line = ws.rep0 >> postcode >> ws >> city >> eol
61
+ postcode = digits.map { |s| { postcode: s.strip } }.capture!
62
+ city = alpha_with_lt.rep.map(&:join).map { |s| { city: s.strip } }.capture!
63
+ city_line = ws.rep0 & postcode & ws & city & eol
64
64
 
65
65
  # Full letter parser which is combined from all previous parsers. All previous parsers can be ran separately.
66
- letter_parser = name_line >> second_line >> address_line >> city_line
66
+ letter_parser = name_line & second_line & address_line & city_line
67
67
  result = letter_parser.run(letter)
68
68
 
69
69
  pp result.captures
data/lib/dolos/parsers.rb CHANGED
@@ -3,9 +3,10 @@
3
3
  module Dolos
4
4
  module Parsers
5
5
  def string(str)
6
+ utf8_str = str.encode('UTF-8')
7
+
6
8
  Parser.new do |state|
7
9
  state.input.mark_offset
8
- utf8_str = str.encode('UTF-8')
9
10
  if state.input.matches?(utf8_str)
10
11
  Success.new(utf8_str, str.bytesize)
11
12
  else
@@ -13,7 +14,7 @@ module Dolos
13
14
  got_error = state.input.io.string.byteslice(state.input.backup, advanced)
14
15
  state.input.rollback
15
16
  Failure.new(
16
- "Expected #{str.inspect} but got #{got_error.inspect}",
17
+ -> { "Expected #{str.inspect} but got #{got_error.inspect}" },
17
18
  advanced,
18
19
  state
19
20
  )
@@ -32,7 +33,7 @@ module Dolos
32
33
  advanced = state.input.offset
33
34
  state.input.rollback
34
35
  Failure.new(
35
- "Expected pattern #{pattern.inspect} but got #{state.input.io.string.inspect}",
36
+ -> { "Expected pattern #{pattern.inspect} but got #{state.input.io.string.inspect}" },
36
37
  advanced,
37
38
  state
38
39
  )
@@ -52,7 +53,7 @@ module Dolos
52
53
  advanced = state.input.offset
53
54
  state.input.rollback
54
55
  Failure.new(
55
- 'Expected any character but got end of input',
56
+ -> { 'Expected any character but got end of input' },
56
57
  advanced,
57
58
  state
58
59
  )
@@ -64,20 +65,20 @@ module Dolos
64
65
  # Example:
65
66
  # char_in('abc').run('b') # => Success.new('b', 1)
66
67
  def char_in(characters_string)
67
- characters_array = characters_string.chars
68
+ characters_set = characters_string.chars
68
69
 
69
70
  Parser.new do |state|
70
71
  state.input.mark_offset
71
72
 
72
73
  char, bytesize = state.input.peek(1)
73
74
 
74
- if char && characters_array.include?(char)
75
+ if char && characters_set.include?(char)
75
76
  Success.new(char, bytesize)
76
77
  else
77
78
  advanced = state.input.offset
78
79
  state.input.rollback
79
80
  Failure.new(
80
- "Expected one of #{characters_array.inspect} but got #{char.inspect}",
81
+ -> { "Expected one of #{characters_set.to_a.inspect} but got #{char.inspect}" },
81
82
  advanced,
82
83
  state
83
84
  )
@@ -90,18 +91,18 @@ module Dolos
90
91
  state.input.mark_offset
91
92
 
92
93
  buffer = String.new
93
- loop do
94
- char, bytesize = state.input.peek(1)
95
- break if char.nil? || !predicate.call(char)
94
+ char, bytesize = state.input.peek(1)
96
95
 
96
+ while char && predicate.call(char)
97
97
  buffer << char
98
98
  state.input.advance(bytesize)
99
+ char, bytesize = state.input.peek(1)
99
100
  end
100
101
 
101
102
  if buffer.empty?
102
103
  advanced = state.input.offset
103
104
  Failure.new(
104
- "Predicate never returned true",
105
+ -> { "Predicate never returned true" },
105
106
  advanced,
106
107
  state
107
108
  )
@@ -111,5 +112,23 @@ module Dolos
111
112
  end
112
113
  end
113
114
 
115
+ def recursive(&block)
116
+ recursive_parser = nil
117
+
118
+ placeholder = Parser.new do |state|
119
+ raise "Recursive parser accessed before it was initialized!" if recursive_parser.nil?
120
+
121
+ recursive_parser.call.run_with_state(state).tap do |result|
122
+ if result.failure?
123
+ error_msg = -> { "Error in recursive structure around position #{state.input.offset}: #{result.message}" }
124
+ Failure.new(error_msg, state.input.offset, state)
125
+ end
126
+ end
127
+ end
128
+
129
+ recursive_parser = -> { block.call(placeholder) }
130
+ placeholder
131
+ end
132
+
114
133
  end
115
134
  end
data/lib/dolos/result.rb CHANGED
@@ -10,20 +10,21 @@ module Dolos
10
10
  def initialize(value, length, captures = [])
11
11
  @value = value
12
12
  @length = length
13
- # @captures = captures || value
14
13
  @captures = captures
15
14
  end
16
15
 
17
- def capture!
18
- if value.is_a?(Array)
19
- value.each do |v|
20
- captures << v
21
- end
16
+ # can be some named capture, :street, {:street => capture }
17
+ # or an array, [], [capture]
18
+ def capture!(wrap_in = nil)
19
+ mapped_value = self.value # use the transformed value here
20
+
21
+ if wrap_in.is_a?(Array)
22
+ save_capture([mapped_value])
23
+ elsif wrap_in.is_a?(Symbol)
24
+ save_capture({ wrap_in => mapped_value })
22
25
  else
23
- captures << value
26
+ save_capture(mapped_value)
24
27
  end
25
-
26
- Success.new(value, length, captures)
27
28
  end
28
29
 
29
30
  def inspect
@@ -37,15 +38,38 @@ module Dolos
37
38
  def failure?
38
39
  false
39
40
  end
41
+
42
+ private
43
+
44
+ def save_capture(val)
45
+ if val.is_a?(Array)
46
+ val.each do |v|
47
+ captures << v
48
+ end
49
+ else
50
+ captures << val
51
+ end
52
+
53
+ Success.new(val, length, captures)
54
+ end
40
55
  end
41
56
 
42
57
  class Failure < Result
43
- attr_reader :message, :error_position, :state
58
+ attr_reader :error_position, :state
44
59
 
45
- def initialize(message, error_position, state)
46
- @message = message
60
+ def initialize(message_proc, error_position, state)
61
+ @message_proc = message_proc
47
62
  @error_position = error_position
48
63
  @state = state
64
+ @message_evaluated = false
65
+ end
66
+
67
+ def message
68
+ unless @message_evaluated
69
+ @message_value = @message_proc.call
70
+ @message_evaluated = true
71
+ end
72
+ @message_value
49
73
  end
50
74
 
51
75
  def inspect
@@ -22,12 +22,7 @@ module Dolos
22
22
 
23
23
  def matches?(utf8_str)
24
24
  read = io.read(utf8_str.bytesize)
25
-
26
- if read.nil?
27
- false
28
- else
29
- read.force_encoding('UTF-8') == utf8_str
30
- end
25
+ !read.nil? && read.force_encoding('UTF-8') == utf8_str
31
26
  end
32
27
 
33
28
  def advance(bytesize)
@@ -61,8 +56,8 @@ module Dolos
61
56
  remaining_data = io.read
62
57
  io.seek(current_position)
63
58
 
64
- if (match_data = remaining_data.match(/\A#{pattern}/))
65
- matched_string = match_data[0]
59
+ if remaining_data =~ /\A#{pattern}/
60
+ matched_string = $&
66
61
  io.seek(current_position + matched_string.bytesize)
67
62
  return matched_string
68
63
  end
data/lib/dolos/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dolos
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/dolos.rb CHANGED
@@ -10,9 +10,7 @@ module Dolos
10
10
  include Parsers
11
11
 
12
12
  class Parser
13
-
14
13
  attr_accessor :parser_proc
15
-
16
14
  def initialize(&block)
17
15
  @parser_proc = block
18
16
  end
@@ -22,54 +20,42 @@ module Dolos
22
20
  end
23
21
 
24
22
  def run_with_state(state)
25
- result = parser_proc.call(state)
26
- if result.success?
27
- state.last_success_position = state.input.offset
28
- end
23
+ result = @parser_proc.call(state)
24
+ state.last_success_position = state.input.offset if result.success?
29
25
  result
30
26
  end
31
27
 
32
- def capture!
28
+ def capture!(wrap_in = nil)
33
29
  Parser.new do |state|
34
30
  result = run_with_state(state)
35
- if result.success?
36
- result.capture!
37
- else
38
- result
39
- end
31
+ result.success? ? result.capture!(wrap_in) : result
40
32
  end
41
33
  end
42
34
 
43
- def map(&block)
35
+ # Will call `map` on captures
36
+ def map_captures(&block)
44
37
  Parser.new do |state|
45
38
  result = run_with_state(state)
46
- if result.success?
47
- Success.new(result.value, result.length, block.call(result.captures))
48
- else
49
- result
50
- end
39
+ result.success? ? Success.new(result.value, result.length, block.call(result.captures)) : result
51
40
  end
52
41
  end
53
42
 
54
- def map_value(&block)
43
+ # Will call block on tuple of value
44
+ def map(&block)
55
45
  Parser.new do |state|
56
46
  result = run_with_state(state)
57
- if result.success?
58
- Success.new(block.call(result.value), result.length, result.captures)
59
- else
60
- result
61
- end
47
+ result.success? ? Success.new(block.call(result.value), result.length, result.captures) : result
62
48
  end
63
49
  end
64
50
 
65
- def flat_map(&block)
51
+ def combine(&block)
66
52
  Parser.new do |state|
67
53
  result = run_with_state(state)
54
+
68
55
  if result.success?
56
+ state.input.advance(result.length)
69
57
  new_parser = block.call(result.value, result.captures)
70
- new_state = state.dup
71
- new_state.input.advance(result.length)
72
- new_parser.run_with_state(new_state)
58
+ new_parser.run_with_state(state)
73
59
  else
74
60
  result
75
61
  end
@@ -77,22 +63,44 @@ module Dolos
77
63
  end
78
64
 
79
65
  def flatten
80
- map do |captures|
66
+ map_captures do |captures|
81
67
  captures.flatten
82
68
  end
83
69
  end
84
70
 
85
71
  def product(other_parser)
86
- flat_map do |value1, capture1|
87
- other_parser.map_value do |value2|
72
+ combine do |value1, capture1|
73
+ other_parser.map do |value2|
88
74
  [value1, value2]
89
- end.map do |capture2|
75
+ end.map_captures do |capture2|
90
76
  [capture1, capture2].flatten
91
77
  end
92
78
  end
93
79
  end
80
+ alias_method :&, :product
94
81
 
95
- alias_method :>>, :product
82
+ def product_l(other_parser)
83
+ combine do |value1, capture1|
84
+ other_parser.map do |_|
85
+ value1
86
+ end.map_captures do |capture2|
87
+ [capture1, capture2].flatten
88
+ end
89
+ end
90
+ end
91
+
92
+ def product_r(other_parser)
93
+ combine do |_, capture1|
94
+ other_parser.map do |value2|
95
+ value2
96
+ end.map_captures do |capture2|
97
+ [capture1, capture2].flatten
98
+ end
99
+ end
100
+ end
101
+
102
+ alias_method :<<, :product_l
103
+ alias_method :>>, :product_r
96
104
 
97
105
  def choice(other_parser)
98
106
  Parser.new do |state|
@@ -111,29 +119,36 @@ module Dolos
111
119
  # rep(n = 2) # exactly 2
112
120
  # repeat(n_min: 2, n_max: 4) # 2 to 4
113
121
  # repeat(n_min: 2) # 2 or more
114
- def repeat(n_min:, n_max: Float::INFINITY)
122
+ def repeat(n_min:, n_max: Float::INFINITY, separator: nil)
115
123
  Parser.new do |state|
116
124
  values = []
117
125
  captures = []
118
126
  count = 0
119
- state.input.mark_offset
120
127
 
121
- while count < n_max
122
- result = run_with_state(state.dup)
128
+ loop do
129
+ result = run_with_state(state) # Removing .dup for performance. Be cautious of side effects.
123
130
 
124
- break if result.failure?
131
+ if result.failure? || count >= n_max
132
+ break
133
+ end
125
134
 
126
135
  values << result.value
127
136
  captures.concat(result.captures)
128
137
  state.input.advance(result.length)
129
138
  count += 1
139
+
140
+ if separator && count < n_max
141
+ sep_result = separator.run_with_state(state) # Removing .dup for performance. Be cautious of side effects.
142
+ break if sep_result.failure?
143
+
144
+ state.input.advance(sep_result.length)
145
+ end
130
146
  end
131
147
 
132
148
  if count < n_min
133
- error_pos = state.input.offset
134
149
  Failure.new(
135
- "Expected parser to match at least #{n_min} times but matched only #{count} times",
136
- error_pos,
150
+ -> { "Expected parser to match at least #{n_min} times but matched only #{count} times" },
151
+ state.input.offset,
137
152
  state
138
153
  )
139
154
  else
@@ -141,7 +156,6 @@ module Dolos
141
156
  end
142
157
  end
143
158
  end
144
-
145
159
  def zero_or_more
146
160
  repeat(n_min: 0, n_max: Float::INFINITY)
147
161
  end
@@ -168,5 +182,15 @@ module Dolos
168
182
  end
169
183
  alias_method :opt, :optional
170
184
 
185
+ # Used to declare lazy parser to avoid infinite loops in recursive parsers
186
+ def lazy
187
+ parser_memo = nil
188
+
189
+ Parser.new do |state|
190
+ parser_memo ||= self
191
+ parser_memo.run_with_state(state)
192
+ end
193
+ end
194
+
171
195
  end
172
196
  end
@@ -6,13 +6,20 @@ module Dolos
6
6
  regex(/\s/)
7
7
  end
8
8
 
9
+ def ws_rep0
10
+ regex(/\s*/)
11
+ end
12
+
9
13
  def eol
10
14
  regex(/\n|\r\n|\r/)
11
15
  end
12
16
 
13
- # Capture as String and convert to integer
14
17
  def digit
15
- regex(/\d/).capture!.map { |capt| capt.map(&:to_i) }
18
+ regex(/\d/)
19
+ end
20
+
21
+ def int
22
+ digit.map(&:to_i)
16
23
  end
17
24
 
18
25
  # Capture as string
@@ -1,5 +1,16 @@
1
1
  module Dolos
2
2
  module CommonParsers
3
+ def digit: -> Parser[String]
4
+ def digits: -> Parser[String]
5
+
6
+ def int: -> Parser[Integer]
7
+
8
+ def eol: -> Parser[String]
9
+
3
10
  def ws: -> Parser[String]
11
+ def ws_rep0: -> Parser[String]
12
+
13
+ def alpha: -> Parser[String]
14
+ def alphanum: -> Parser[String]
4
15
  end
5
16
  end
data/sig/dolos/parser.rbs CHANGED
@@ -4,16 +4,20 @@ module Dolos
4
4
  def initialize: (^(ParserState) -> Result[A]) -> Parser[A]
5
5
  def capture!: -> Parser[A]
6
6
  def choice: [B](Parser[B])-> Parser[A | B]
7
+ def combine: [B](^(A, B) -> Parser[B]) -> Parser[B]
7
8
  def flat_map: [B](Parser[A], ^(A) -> Parser[B]) -> Parser[B]
8
9
  def flatten: -> Parser[A]
9
10
  def map: [B](^(A) -> B) -> Parser[B]
10
- def map_value: [B](^(A) -> B) -> Parser[B]
11
+ def map_captures: [B](^(A) -> B) -> Parser[B]
11
12
  def optional: -> Parser[A?]
12
13
  def product: [B](Parser[A]) -> Parser[B]
14
+ def product_l: [B](Parser[B]) -> Parser[B]
15
+ def product_r: [B](Parser[B]) -> Parser[A]
13
16
  def run: (String) -> Result[A]
14
17
  def run_with_state: (ParserState) -> Result[A]
15
- def repeat: (Integer, Integer)-> Parser[Array[A]]
18
+ def repeat: [B](Integer, Integer, Parser[B]?)-> Parser[Array[A]]
16
19
  def zero_or_more: -> Parser[Array[A]]
17
20
  def one_or_more: (Integer?) -> Parser[Array[A]]
21
+ def lazy: -> Parser[A]
18
22
  end
19
23
  end
@@ -1,6 +1,6 @@
1
1
  module Dolos
2
2
  class ParserState
3
- attr_reader input: Dolos::StringIOWrapper
3
+ attr_reader input: StringIOWrapper
4
4
  attr_accessor last_success_position: Integer
5
5
 
6
6
  def initialize: (String) -> void
@@ -1,6 +1,10 @@
1
1
  module Dolos
2
2
  module Parsers
3
3
  def any_char: -> Parser[String]
4
+ def char_in: -> Parser[String]
5
+ def char_while : -> Parser[String]
6
+ def recursive: [A,B,C]() { (Parser[A]) -> Parser[B] } -> Parser[C]
7
+
4
8
  def regex: (Regexp) -> Parser[String]
5
9
  def string: (String)-> Parser[String]
6
10
  end
data/sig/dolos/result.rbs CHANGED
@@ -15,6 +15,11 @@ module Dolos
15
15
  end
16
16
 
17
17
  class Failure < Result[bot]
18
+ @message_proc: ^-> String
19
+ @message_evaluated: bool
20
+ @message_value: String
21
+ @state: ParserState
22
+
18
23
  attr_reader committed: bool
19
24
  attr_reader error_position: Integer
20
25
  attr_reader message: String
@@ -25,6 +30,8 @@ module Dolos
25
30
 
26
31
  def map: [B](^(bot) -> B) -> Result[B]
27
32
 
33
+ def pretty_print: -> String
34
+
28
35
  def success?: -> bool
29
36
  end
30
37
  end