def read(num_elements, from: nil, as: nil, force_array: false)
      stream.pos = from if from
      return stream.read(num_elements) if as.nil?

      conv = to_proc(as, :read)
      # TODO: handle eof
      ret = Array.new(num_elements) { conv.call(stream) }
      ret = ret.first if num_elements == 1 && !force_array
      ret
    end
def read(num_elements, from: nil, as: nil, force_array: false)
      stream.pos = from if from
      return stream.read(num_elements) if as.nil?

      conv = to_proc(as, :read)
      # TODO: handle eof
      ret = Array.new(num_elements) { conv.call(stream) }
      ret = ret.first if num_elements == 1 && !force_array
      ret
    end
@return [#pos, #pos=, #read, #write]
 Instantiate an {IO} object.

 @param [#pos, #pos=, #read, #write] stream
   The file-like object to be read/written.
   +file+ can be unwritable if you will not invoke any write-related method.

   If +stream.read(*)+ returns empty string or +nil+, it would be seen as reaching EOF.
 Read and convert result into custom type/stucture.

 @param [Integer] num_elements
   Number of elements to be read.
   This parameter must be positive and larger than zero.

   This parameter may effect the return type,
   see documents of return value.
 @param [Integer?] from
   Invoke +stream.pos = from+ before starting to read.
   +nil+ for not changing current position of stream.
 @param [nil, Symbol, Proc] as
   Decide the type/structure when reading.
   See {MemoryIO::Types} for all supported types.

   A +Proc+ is allowed, which should accept +stream+ as the first argument.
   The return value of the proc would be the return objects of this method.

   If +nil+ is given, this method returns a string and has same behavior as +::IO#read+.
 @param [Boolean] force_array
   When +num_elements+ equals to 1, the read +Object+ would be returned.
   Pass +true+ to this parameter to force this method returning an array.

 @return [String, Object, Array<Object>]
   There're multiple possible return types,
   which dependes on the value of parameter +num_elements+, +as+, and +force_array+.

   See examples for clear usage. The rule of return type is listed as following:

   * +as = nil+:
     A +String+ with length +num_elements+ is returned.
   * +as != nil+ and +num_elements = 1+ and +force_array = false+:
     An +Object+ is returned. The type of +Object+ depends on parameter +as+.
   * +as != nil+ and +num_elements = 1+ and +force_array = true+:
     An array with one element is returned.
   * +as != nil+ and +num_elements > 1+:
     An array with length +num_elements+ is returned.

   If EOF is occured, object(s) read will be returned.

 @example
   stream = StringIO.new('A' * 8 + 'B' * 8)
   io = MemoryIO::IO.new(stream)
   io.read(9)
   #=> "AAAAAAAAB"
   io.read(100)
   #=> "BBBBBBB"

   # read two unsigned 32-bit integers starts from posistion 4
   io.read(2, from: 4, as: :u32)
   #=> [1094795585, 1111638594] # [0x41414141, 0x42424242]

   io.read(1, as: :u16)
   #=> 16962 # 0x4242
   io.read(1, as: :u16, force_array: true)
   #=> [16962]
 @example
   stream = StringIO.new("\xef\xbe\xad\xde")
   io = MemoryIO::IO.new(stream)
   io.read(1, as: :u32)
   #=> 3735928559
   io.rewind
   io.read(1, as: :s32)
   #=> -559038737
 @example
   stream = StringIO.new("123\x0045678\x00")
   io = MemoryIO::IO.new(stream)
   io.read(2, as: :c_str)
   #=> ["123", "45678"]
 @example
   # pass lambda to `as`
   stream = StringIO.new("\x03123\x044567")
   io = MemoryIO::IO.new(stream)
   io.read(2, as: lambda { |stream| stream.read(stream.read(1).ord) })
   #=> ["123", "4567"]

 @note
   This method's arguments and return value are different with +::IO#read+.
   Check documents and examples.

 @see Types